Associação OneWayToSource da propriedade somente leitura em XAML

87

Estou tentando vincular a uma Readonlypropriedade com OneWayToSourceo modo, mas parece que isso não pode ser feito em XAML:

<controls:FlagThingy IsModified="{Binding FlagIsModified, 
                                          ElementName=container, 
                                          Mode=OneWayToSource}" />

Eu recebo:

A propriedade 'FlagThingy.IsModified' não pode ser definida porque não possui um acessador de conjunto acessível.

IsModifiedé um de somente leitura DependencyPropertyem FlagThingy. Quero vincular esse valor à FlagIsModifiedpropriedade no contêiner.

Para ser claro:

FlagThingy.IsModified --> container.FlagIsModified
------ READONLY -----     ----- READWRITE --------

Isso é possível usando apenas XAML?


Atualização: Bem, eu consertei este caso definindo a ligação no contêiner e não no FlagThingy. Mas ainda gostaria de saber se isso é possível.

Inferis
fonte
Mas como você pode definir o valor para uma propriedade somente leitura?
idursun
3
Você não pode. Também não é o que estou tentando alcançar. Estou tentando obter FROM propriedade somente leitura IsModifiedpara propriedade readwrite FlagIsModified.
Inferis
Boa pergunta. Sua solução alternativa só funciona se o contêiner for um DependencyObject e FlagIsModified for um DependencyProperty.
Josh G
10
Ótima pergunta, porém não consigo entender a resposta aceita. Eu apreciaria se algum guru do WPF pudesse me esclarecer um pouco mais - isso é um bug ou por design?
Oskar
@Oskar de acordo com isso é um bug. nenhuma correção à vista embora.
user1151923

Respostas:

45

Alguns resultados de pesquisa para OneWayToSource ...

Opção 1.

// Control definition
public partial class FlagThingy : UserControl
{
    public static readonly DependencyProperty IsModifiedProperty = 
            DependencyProperty.Register("IsModified", typeof(bool), typeof(FlagThingy), new PropertyMetadata());
}
<controls:FlagThingy x:Name="_flagThingy" />
// Binding Code
Binding binding = new Binding();
binding.Path = new PropertyPath("FlagIsModified");
binding.ElementName = "container";
binding.Mode = BindingMode.OneWayToSource;
_flagThingy.SetBinding(FlagThingy.IsModifiedProperty, binding);

Opção 2

// Control definition
public partial class FlagThingy : UserControl
{
    public static readonly DependencyProperty IsModifiedProperty = 
            DependencyProperty.Register("IsModified", typeof(bool), typeof(FlagThingy), new PropertyMetadata());

    public bool IsModified
    {
        get { return (bool)GetValue(IsModifiedProperty); }
        set { throw new Exception("An attempt ot modify Read-Only property"); }
    }
}
<controls:FlagThingy IsModified="{Binding Path=FlagIsModified, 
    ElementName=container, Mode=OneWayToSource}" />

Opção nº 3 (propriedade de dependência somente leitura verdadeira)

System.ArgumentException: a propriedade 'IsModified' não pode ser vinculada a dados.

// Control definition
public partial class FlagThingy : UserControl
{
    private static readonly DependencyPropertyKey IsModifiedKey =
        DependencyProperty.RegisterReadOnly("IsModified", typeof(bool), typeof(FlagThingy), new PropertyMetadata());

    public static readonly DependencyProperty IsModifiedProperty = 
        IsModifiedKey.DependencyProperty;
}
<controls:FlagThingy x:Name="_flagThingy" />
// Binding Code
Same binding code...

O Reflector dá a resposta:

internal static BindingExpression CreateBindingExpression(DependencyObject d, DependencyProperty dp, Binding binding, BindingExpressionBase parent)
{
    FrameworkPropertyMetadata fwMetaData = dp.GetMetadata(d.DependencyObjectType) as FrameworkPropertyMetadata;
    if (((fwMetaData != null) && !fwMetaData.IsDataBindingAllowed) || dp.ReadOnly)
    {
        throw new ArgumentException(System.Windows.SR.Get(System.Windows.SRID.PropertyNotBindable, new object[] { dp.Name }), "dp");
    }
 ....
alex2k8
fonte
30
Então isso é um bug, na verdade.
Inferis
Boa pesquisa. Se você não tivesse colocado isso tão bem aqui, eu teria trilhado o mesmo caminho doloroso. Concordo com @Inferis.
kevinarpe
1
Isso é um inseto? Por que uma associação OneWayToSource não seria permitida com uma DependencyProperty somente leitura?
Alex Hope O'Connor
Este não é um bug. É intencional e bem documentado. É devido à maneira como o mecanismo de ligação funciona em conjunto com o sistema de propriedade de dependência (o destino da ligação deve ser um DependencyPropertyDP). Um DP somente leitura só pode ser modificado usando o associado DependencyPropertyKey. Para registrar um, BindingExpressiono mecanismo deve manipular os metadados do DP de destino. Como DependencyPropertyKeyé considerado privado para garantir a proteção pública contra gravação, o mecanismo terá que ignorar essa chave com o resultado de não ser capaz de registrar a vinculação em um DP somente leitura.
BionicCode
23

Esta é uma limitação do WPF e é por design. É relatado no Connect aqui:
ligação OneWayToSource de uma propriedade de dependência somente leitura

Eu criei uma solução para poder dinamicamente enviar propriedades de dependência somente leitura para a fonte chamada, a PushBindingqual bloguei aqui . O exemplo abaixo faz OneWayToSourceligações dos DPs somente leitura ActualWidthe ActualHeightpara as propriedades de largura e altura doDataContext

<TextBlock Name="myTextBlock">
    <pb:PushBindingManager.PushBindings>
        <pb:PushBinding TargetProperty="ActualHeight" Path="Height"/>
        <pb:PushBinding TargetProperty="ActualWidth" Path="Width"/>
    </pb:PushBindingManager.PushBindings>
</TextBlock>

PushBindingfunciona usando duas propriedades de dependência, ouvinte e espelho. O ouvinte é vinculado OneWaya TargetProperty e PropertyChangedCallbackatualiza a propriedade Mirror, que é vinculada OneWayToSourcea tudo o que foi especificado em Binding.

O projeto de demonstração pode ser baixado aqui.
Ele contém código-fonte e uso de amostra curta.

Fredrik Hedblad
fonte
Interessante! Eu vim com uma solução semelhante e chamei-o de "Conduit" - o Conduit tinha duas propriedades de dependência de acordo com seu projeto e duas ligações separadas. O caso de uso que tive era vincular propriedades simples e antigas a propriedades simples e antigas em XAML.
Daniel Paull
3
Vejo que o seu link MS Connect não funciona mais. Isso significa que a MS o corrigiu na versão mais recente do .NET ou simplesmente o excluiu?
Tiny
@Tiny Connect parece ter sido eventualmente abandonado, infelizmente. Ele foi conectado em vários lugares. Não acho que isso implique especificamente em saber se um problema foi corrigido.
UuDdLrLrSs
Eu estava prestes a escrever exatamente isso. Bom trabalho!
aaronburro
5

Escreveu isto:

Uso:

<TextBox Text="{Binding Text}"
         p:OneWayToSource.Bind="{p:Paths From={x:Static Validation.HasErrorProperty},
                                         To=SomeDataContextProperty}" />

Código:

using System;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;

public static class OneWayToSource
{
    public static readonly DependencyProperty BindProperty = DependencyProperty.RegisterAttached(
        "Bind",
        typeof(ProxyBinding),
        typeof(OneWayToSource),
        new PropertyMetadata(default(Paths), OnBindChanged));

    public static void SetBind(this UIElement element, ProxyBinding value)
    {
        element.SetValue(BindProperty, value);
    }

    [AttachedPropertyBrowsableForChildren(IncludeDescendants = false)]
    [AttachedPropertyBrowsableForType(typeof(UIElement))]
    public static ProxyBinding GetBind(this UIElement element)
    {
        return (ProxyBinding)element.GetValue(BindProperty);
    }

    private static void OnBindChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((ProxyBinding)e.OldValue)?.Dispose();
    }

    public class ProxyBinding : DependencyObject, IDisposable
    {
        private static readonly DependencyProperty SourceProxyProperty = DependencyProperty.Register(
            "SourceProxy",
            typeof(object),
            typeof(ProxyBinding),
            new PropertyMetadata(default(object), OnSourceProxyChanged));

        private static readonly DependencyProperty TargetProxyProperty = DependencyProperty.Register(
            "TargetProxy",
            typeof(object),
            typeof(ProxyBinding),
            new PropertyMetadata(default(object)));

        public ProxyBinding(DependencyObject source, DependencyProperty sourceProperty, string targetProperty)
        {
            var sourceBinding = new Binding
            {
                Path = new PropertyPath(sourceProperty),
                Source = source,
                Mode = BindingMode.OneWay,
            };

            BindingOperations.SetBinding(this, SourceProxyProperty, sourceBinding);

            var targetBinding = new Binding()
            {
                Path = new PropertyPath($"{nameof(FrameworkElement.DataContext)}.{targetProperty}"),
                Mode = BindingMode.OneWayToSource,
                Source = source
            };

            BindingOperations.SetBinding(this, TargetProxyProperty, targetBinding);
        }

        public void Dispose()
        {
            BindingOperations.ClearAllBindings(this);
        }

        private static void OnSourceProxyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            d.SetCurrentValue(TargetProxyProperty, e.NewValue);
        }
    }
}

[MarkupExtensionReturnType(typeof(OneWayToSource.ProxyBinding))]
public class Paths : MarkupExtension
{
    public DependencyProperty From { get; set; }

    public string To { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var provideValueTarget = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
        var targetObject = (UIElement)provideValueTarget.TargetObject;
        return new OneWayToSource.ProxyBinding(targetObject, this.From, this.To);
    }
}

Ainda não testei em estilos e modelos, acho que precisa de um invólucro especial.

Johan Larsson
fonte
2

Aqui está outra solução de propriedade anexada com base em SizeObserver detalhada aqui Empurrando propriedades GUI somente leitura de volta para ViewModel

public static class MouseObserver
{
    public static readonly DependencyProperty ObserveProperty = DependencyProperty.RegisterAttached(
        "Observe",
        typeof(bool),
        typeof(MouseObserver),
        new FrameworkPropertyMetadata(OnObserveChanged));

    public static readonly DependencyProperty ObservedMouseOverProperty = DependencyProperty.RegisterAttached(
        "ObservedMouseOver",
        typeof(bool),
        typeof(MouseObserver));


    public static bool GetObserve(FrameworkElement frameworkElement)
    {
        return (bool)frameworkElement.GetValue(ObserveProperty);
    }

    public static void SetObserve(FrameworkElement frameworkElement, bool observe)
    {
        frameworkElement.SetValue(ObserveProperty, observe);
    }

    public static bool GetObservedMouseOver(FrameworkElement frameworkElement)
    {
        return (bool)frameworkElement.GetValue(ObservedMouseOverProperty);
    }

    public static void SetObservedMouseOver(FrameworkElement frameworkElement, bool observedMouseOver)
    {
        frameworkElement.SetValue(ObservedMouseOverProperty, observedMouseOver);
    }

    private static void OnObserveChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        var frameworkElement = (FrameworkElement)dependencyObject;
        if ((bool)e.NewValue)
        {
            frameworkElement.MouseEnter += OnFrameworkElementMouseOverChanged;
            frameworkElement.MouseLeave += OnFrameworkElementMouseOverChanged;
            UpdateObservedMouseOverForFrameworkElement(frameworkElement);
        }
        else
        {
            frameworkElement.MouseEnter -= OnFrameworkElementMouseOverChanged;
            frameworkElement.MouseLeave -= OnFrameworkElementMouseOverChanged;
        }
    }

    private static void OnFrameworkElementMouseOverChanged(object sender, MouseEventArgs e)
    {
        UpdateObservedMouseOverForFrameworkElement((FrameworkElement)sender);
    }

    private static void UpdateObservedMouseOverForFrameworkElement(FrameworkElement frameworkElement)
    {
        frameworkElement.SetCurrentValue(ObservedMouseOverProperty, frameworkElement.IsMouseOver);
    }
}

Declarar propriedade anexada no controle

<ListView ItemsSource="{Binding SomeGridItems}"                             
     ut:MouseObserver.Observe="True"
     ut:MouseObserver.ObservedMouseOver="{Binding IsMouseOverGrid, Mode=OneWayToSource}">    
jv_
fonte
1

Aqui está outra implementação para vincular a Validation.HasError

public static class OneWayToSource
{
    public static readonly DependencyProperty BindingsProperty = DependencyProperty.RegisterAttached(
        "Bindings",
        typeof(OneWayToSourceBindings),
        typeof(OneWayToSource),
        new PropertyMetadata(default(OneWayToSourceBindings), OnBinidngsChanged));

    public static void SetBindings(this FrameworkElement element, OneWayToSourceBindings value)
    {
        element.SetValue(BindingsProperty, value);
    }

    [AttachedPropertyBrowsableForChildren(IncludeDescendants = false)]
    [AttachedPropertyBrowsableForType(typeof(FrameworkElement))]
    public static OneWayToSourceBindings GetBindings(this FrameworkElement element)
    {
        return (OneWayToSourceBindings)element.GetValue(BindingsProperty);
    }

    private static void OnBinidngsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((OneWayToSourceBindings)e.OldValue)?.ClearValue(OneWayToSourceBindings.ElementProperty);
        ((OneWayToSourceBindings)e.NewValue)?.SetValue(OneWayToSourceBindings.ElementProperty, d);
    }
}

public class OneWayToSourceBindings : FrameworkElement
{
    private static readonly PropertyPath DataContextPath = new PropertyPath(nameof(DataContext));
    private static readonly PropertyPath HasErrorPath = new PropertyPath($"({typeof(Validation).Name}.{Validation.HasErrorProperty.Name})");
    public static readonly DependencyProperty HasErrorProperty = DependencyProperty.Register(
        nameof(HasError),
        typeof(bool),
        typeof(OneWayToSourceBindings),
        new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

    internal static readonly DependencyProperty ElementProperty = DependencyProperty.Register(
        "Element",
        typeof(UIElement),
        typeof(OneWayToSourceBindings),
        new PropertyMetadata(default(UIElement), OnElementChanged));

    private static readonly DependencyProperty HasErrorProxyProperty = DependencyProperty.RegisterAttached(
        "HasErrorProxy",
        typeof(bool),
        typeof(OneWayToSourceBindings),
        new PropertyMetadata(default(bool), OnHasErrorProxyChanged));

    public bool HasError
    {
        get { return (bool)this.GetValue(HasErrorProperty); }
        set { this.SetValue(HasErrorProperty, value); }
    }

    private static void OnHasErrorProxyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        d.SetCurrentValue(HasErrorProperty, e.NewValue);
    }

    private static void OnElementChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (e.NewValue == null)
        {
            BindingOperations.ClearBinding(d, DataContextProperty);
            BindingOperations.ClearBinding(d, HasErrorProxyProperty);
        }
        else
        {
            var dataContextBinding = new Binding
                                         {
                                             Path = DataContextPath,
                                             Mode = BindingMode.OneWay,
                                             Source = e.NewValue
                                         };
            BindingOperations.SetBinding(d, DataContextProperty, dataContextBinding);

            var hasErrorBinding = new Binding
                                      {
                                          Path = HasErrorPath,
                                          Mode = BindingMode.OneWay,
                                          Source = e.NewValue
                                      };
            BindingOperations.SetBinding(d, HasErrorProxyProperty, hasErrorBinding);
        }
    }
}

Uso em xaml

<StackPanel>
    <TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}">
        <local:OneWayToSource.Bindings>
            <local:OneWayToSourceBindings HasError="{Binding HasError}" />
        </local:OneWayToSource.Bindings>
    </TextBox>
    <CheckBox IsChecked="{Binding HasError, Mode=OneWay}" />
</StackPanel>

Esta implementação é específica para ligação Validation.HasError

Johan Larsson
fonte
0

O WPF não usará o configurador de propriedade CLR, mas parece que faz algumas validações estranhas com base nele.

Pode ser na sua situação isso pode estar ok:

    public bool IsModified
    {
        get { return (bool)GetValue(IsModifiedProperty); }
        set { throw new Exception("An attempt ot modify Read-Only property"); }
    }
alex2k8
fonte
1
A propriedade CLR não é usada neste caso.
Inferis
Você quer dizer que acabou de definir DependencyProperty e foi capaz de escrever <controls: FlagThingy IsModified = "..." />? Para mim, ele diz: "A propriedade 'IsModified' não existe no namespace XML" se eu não adicionar a propriedade CLR.
alex2k8
1
Eu acredito que o tempo de design usa as propriedades clr onde o tempo de execução vai diretamente para a propriedade de dependência (se for uma).
meandmycode
A propriedade CLR é desnecessária no meu caso (eu não uso IsModified do código), mas está lá mesmo assim (com apenas um setter público). O tempo de design e o tempo de execução funcionam bem apenas com o registro de propriedade de dependência.
Inferis
A vinculação em si não está usando a propriedade CLR, mas quando você define a vinculação em XAML, ela deve ser convertida em código. Acho que neste estágio o analisador XAML vê que a propriedade IsModified é somente leitura e lança uma exceção (mesmo antes da criação da ligação).
alex2k8
0

Hmmm ... Não tenho certeza se concordo com qualquer uma dessas soluções. Que tal especificar um retorno de chamada de coerção em seu registro de propriedade que ignora mudanças externas? Por exemplo, eu precisava implementar uma propriedade de dependência Position somente leitura para obter a posição de um controle MediaElement dentro de um controle de usuário. Veja como eu fiz:

    public static readonly DependencyProperty PositionProperty = DependencyProperty.Register("Position", typeof(double), typeof(MediaViewer),
        new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | FrameworkPropertyMetadataOptions.Journal, OnPositionChanged, OnPositionCoerce));

    private static void OnPositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var ctrl = d as MediaViewer;
    }

    private static object OnPositionCoerce(DependencyObject d, object value)
    {
        var ctrl = d as MediaViewer;
        var position = ctrl.MediaRenderer.Position.TotalSeconds;

        if (ctrl.MediaRenderer.NaturalDuration.HasTimeSpan == false)
            return 0d;
        else
            return Math.Min(position, ctrl.Duration);
    }

    public double Position
    {
        get { return (double)GetValue(PositionProperty); }
        set { SetValue(PositionProperty, value); }
    }

Em outras palavras, simplesmente ignore a alteração e retorne o valor apoiado por um membro diferente que não possui um modificador público. - No exemplo acima, MediaRenderer é realmente o controle MediaElement privado.

Mario
fonte
Uma pena que isso não funcione para propriedades predefinidas de classes BCL: - /
OR Mapper
0

A maneira como contornei essa limitação foi expor apenas uma propriedade Binding em minha classe, mantendo DependencyProperty totalmente privado. Implementei uma propriedade somente gravação "PropertyBindingToSource" (esta não é uma DependencyProperty) que pode ser definida como um valor de ligação no xaml. No setter desta propriedade somente gravação, chamo BindingOperations.SetBinding para vincular a vinculação à DependencyProperty.

Para o exemplo específico do OP, seria assim:

A implementação FlatThingy:

public partial class FlatThingy : UserControl
{
    public FlatThingy()
    {
        InitializeComponent();
    }

    public Binding IsModifiedBindingToSource
    {
        set
        {
            if (value?.Mode != BindingMode.OneWayToSource)
            {
                throw new InvalidOperationException("IsModifiedBindingToSource must be set to a OneWayToSource binding");
            }

            BindingOperations.SetBinding(this, IsModifiedProperty, value);
        }
    }

    public bool IsModified
    {
        get { return (bool)GetValue(IsModifiedProperty); }
        private set { SetValue(IsModifiedProperty, value); }
    }

    private static readonly DependencyProperty IsModifiedProperty =
        DependencyProperty.Register("IsModified", typeof(bool), typeof(FlatThingy), new PropertyMetadata(false));

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        IsModified = !IsModified;
    }
}

Observe que o objeto DependencyProperty estático somente leitura é privado. No controle adicionei um botão cujo clique é manipulado por Button_Click. O uso do controle FlatThingy em meu window.xaml:

<Window x:Class="ReadOnlyBinding.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:ReadOnlyBinding"
    mc:Ignorable="d"
    DataContext="{x:Static local:ViewModel.Instance}"
    Title="MainWindow" Height="450" Width="800">
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition />
    </Grid.RowDefinitions>

    <TextBlock Text="{Binding FlagIsModified}" Grid.Row="0" />
    <local:FlatThingy IsModifiedBindingToSource="{Binding FlagIsModified, Mode=OneWayToSource}" Grid.Row="1" />
</Grid>

Observe que também implementei um ViewModel para vinculação que não é mostrado aqui. Ele expõe uma DependencyProperty chamada "FlagIsModified", como você pode obter na fonte acima.

Funciona muito bem, permitindo-me enviar informações de volta para o ViewModel a partir da View de maneira fracamente acoplada, com a direção desse fluxo de informações explicitamente definida.

John Thoits
fonte
-1

Você está fazendo a ligação na direção errada agora. OneWayToSource tentará atualizar FlagIsModified no contêiner sempre que IsModified mudar no controle que você está criando. Você quer o oposto, que é ter IsModified vinculado a container.FlagIsModified. Para isso você deve usar o modo de ligação OneWay

<controls:FlagThingy IsModified="{Binding FlagIsModified, 
                                          ElementName=container, 
                                          Mode=OneWay}" />

Lista completa de membros da enumeração: http://msdn.microsoft.com/en-us/library/system.windows.data.bindingmode.aspx

JaredPar
fonte
5
Não, eu quero exatamente o cenário que você descreve e que não quero fazer. FlagThingy.IsModified -> container.FlagIsModified
Inferis
3
Ser marcado porque o questionador tinha uma pergunta ambígua parece um pouco exagero.
JaredPar
6
@JaredPar: Não vejo o que há de ambíguo nessa questão. A questão afirma que 1) há uma propriedade de dependência somente leitura IsIsModified, que 2) o OP deseja declarar uma vinculação nessa propriedade em XAML e que 3) a vinculação deve funcionar no OneWayToSourcemodo. Sua solução não funciona na prática porque, conforme descrito na pergunta, o compilador não permite que você declare um vínculo em uma propriedade somente leitura e não funciona conceitualmente porque IsModifiedé somente leitura e, portanto, seu valor não pode ser alterado (pela ligação).
OR Mapper