Associazione OneWayToSource dalla proprietà readonly in XAML


88

Sto provando a collegarmi a una Readonlyproprietà con OneWayToSourcela modalità as, ma sembra che non sia possibile farlo in XAML:

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

Ottengo:

La proprietà "FlagThingy.IsModified" non può essere impostata perché non dispone di una funzione di accesso set accessibile.

IsModifiedè una sola lettura DependencyPropertysu FlagThingy. Voglio associare quel valore aFlagIsModified proprietà sul contenitore.

Per essere chiari:

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

È possibile utilizzare solo XAML?


Aggiornamento: Bene, ho risolto questo caso impostando l'associazione sul contenitore e non su FlagThingy. Ma mi piacerebbe ancora sapere se questo è possibile.


Ma come puoi impostare il valore su una proprietà di sola lettura?
idursun

3
Non puoi. Inoltre, non è quello che sto cercando di ottenere. Sto cercando di ottenere dalla proprietà IsModifieddi sola lettura alla proprietà di lettura e scrittura FlagIsModified.
Inferis

Buona domanda. La soluzione alternativa funziona solo se il contenitore è un DependencyObject e FlagIsModified è un DependencyProperty.
Josh G

10
Ottima domanda, tuttavia non riesco a capire la risposta accettata. Apprezzerei se qualche guru di WPF potesse illuminarmi di più: è un bug o per progetto?
Oskar

@Oskar secondo questo è un bug. nessuna soluzione in vista però.
user1151923

Risposte:


46

Alcuni risultati della ricerca per OneWayToSource ...

Opzione 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);

Opzione 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}" />

Opzione n. 3 (proprietà di dipendenza di sola lettura vera)

System.ArgumentException: la proprietà "IsModified" non può essere associata a dati.

// 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...

Il riflettore dà la risposta:

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");
    }
 ....

30
Quindi questo è un bug, in realtà.
Inferis

Bella ricerca. Se non l'avessi disposto così bene qui, avrei percorso lo stesso percorso doloroso. D'accordo con @Inferis.
kevinarpe

1
è un insetto? Perché un'associazione OneWayToSource non dovrebbe essere consentita con una DependencyProperty di sola lettura?
Alex Hope O'Connor

Questo non è un bug. È progettato e ben documentato. È a causa del modo in cui il motore di associazione funziona insieme al sistema delle proprietà di dipendenza (l'obiettivo di associazione deve essere un DependencyPropertyDP). Un DP di sola lettura può essere modificato solo utilizzando il file associato DependencyPropertyKey. Per registrare un BindingExpressionmotore deve manipolare i metadati del DP di destinazione. Poiché DependencyPropertyKeyè considerato privato per garantire la protezione da scrittura pubblica, il motore dovrà ignorare questa chiave con il risultato di non essere in grado di registrare l'associazione su un DP di sola lettura.
BionicCode

23

Questa è una limitazione di WPF ed è di progettazione. È riportato su Connect qui:
associazione OneWayToSource da una proprietà di dipendenza di sola lettura

Ho creato una soluzione per essere in grado di inviare dinamicamente le proprietà di dipendenza di sola lettura alla sorgente chiamata di PushBindingcui ho scritto nel blog qui . L'esempio seguente esegue i OneWayToSourcecollegamenti dai DP di sola lettura ActualWidthe ActualHeightalle proprietà Larghezza e Altezza del fileDataContext

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

PushBindingfunziona utilizzando due proprietà di dipendenza, Listener e Mirror. Listener è associato OneWaya TargetProperty e in PropertyChangedCallbackesso aggiorna la proprietà Mirror che è associata OneWayToSourcea tutto ciò che è stato specificato in Binding.

Il progetto demo può essere scaricato qui.
Contiene codice sorgente e un breve utilizzo di esempio.


Interessante! Ho trovato una soluzione simile e l'ho chiamata "Conduit": il Conduit aveva due proprietà di dipendenza come da progetto e due binding separati. Il caso d'uso che ho avuto è stato quello di associare semplici vecchie proprietà a semplici vecchie proprietà in XAML.
Daniel Paull

3
Vedo che il tuo collegamento a MS Connect non funziona più. Significa che MS l'ha risolto nella versione più recente di .NET o l'hanno semplicemente cancellato?
Minuscolo

@Tiny Connect sembra essere stato finalmente abbandonato, purtroppo. È stato collegato in numerosi luoghi. Non penso che implichi specificamente qualcosa sul fatto che un problema sia stato risolto.
UuDdLrLrSs

5

Ha scritto questo:

Utilizzo:

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

Codice:

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);
    }
}

Non l'ho ancora testato in stili e modelli, immagino che abbia bisogno di un involucro speciale.


2

Ecco un'altra soluzione di proprietà collegata basata su SizeObserver descritta in dettaglio qui Reinserimento delle proprietà della GUI di sola lettura in 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);
    }
}

Dichiarare la proprietà associata nel controllo

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

1

Ecco un'altra implementazione per l'associazione 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);
        }
    }
}

Utilizzo in 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>

Questa implementazione è specifica per l'associazione Validation.HasError


0

WPF non utilizzerà il setter di proprietà CLR, ma sembra che esegua qualche strana convalida basata su di esso.

Potrebbe essere nella tua situazione questo può essere ok:

    public bool IsModified
    {
        get { return (bool)GetValue(IsModifiedProperty); }
        set { throw new Exception("An attempt ot modify Read-Only property"); }
    }

1
La proprietà CLR non viene utilizzata in questo caso.
Inferis

Vuoi dire che hai appena definito DependencyProperty e sei riuscito a scrivere <controls: FlagThingy IsModified = "..." />? Per me dice: "La proprietà 'IsModified' non esiste nello spazio dei nomi XML" se non aggiungo la proprietà CLR.
alex2k8

1
Credo che la fase di progettazione utilizzi le proprietà clr dove come runtime in realtà va direttamente alla proprietà di dipendenza (se è una).
meandmycode

La proprietà CLR non è necessaria nel mio caso (non uso IsModified dal codice), ma è comunque lì (con solo un setter pubblico). Sia il tempo di progettazione che il runtime funzionano bene con la sola registrazione della proprietà delle dipendenze.
Inferis

L'associazione stessa non usa la proprietà CLR, ma quando si definisce l'associazione in XAML è necessario tradurla in codice. Immagino che in questa fase il parser XAML veda che la proprietà IsModified è di sola lettura e genera un'eccezione (anche prima della creazione dell'associazione).
alex2k8

0

Hmmm ... Non sono sicuro di essere d'accordo con nessuna di queste soluzioni. Che ne dici di specificare una richiamata coercitiva nella registrazione della tua proprietà che ignora il cambiamento esterno? Ad esempio, avevo bisogno di implementare una proprietà di dipendenza Position di sola lettura per ottenere la posizione di un controllo MediaElement all'interno di un controllo utente. Ecco come l'ho fatto:

    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); }
    }

In altre parole, ignora semplicemente la modifica e restituisci il valore supportato da un membro diverso che non dispone di un modificatore pubblico. - Nell'esempio precedente, MediaRenderer è in realtà il controllo MediaElement privato.


Peccato che questo non funzioni per le proprietà predefinite delle classi BCL: - /
OR Mapper

0

Il modo in cui ho aggirato questa limitazione è stato di esporre solo una proprietà Binding nella mia classe, mantenendo la proprietà DependencyProperty completamente privata. Ho implementato una proprietà di sola scrittura "PropertyBindingToSource" (questa non è una DependencyProperty) che può essere impostata su un valore di associazione in xaml. Nel setter per questa proprietà di sola scrittura chiamo BindingOperations.SetBinding per collegare l'associazione a DependencyProperty.

Per l'esempio specifico dell'OP, sarebbe simile a questo:

L'implementazione di 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;
    }
}

Si noti che l'oggetto DependencyProperty di sola lettura statica è privato. Nel controllo ho aggiunto un pulsante il cui clic è gestito da Button_Click. L'uso del controllo FlatThingy nel mio 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>

Nota che ho anche implementato un ViewModel per l'associazione a quello non mostrato qui. Espone una DependencyProperty denominata "FlagIsModified" come puoi ricavare dalla fonte sopra.

Funziona alla grande, permettendomi di reinserire le informazioni nel ViewModel dalla View in modo vagamente accoppiato, con la direzione del flusso di informazioni esplicitamente definita.


-1

Stai facendo il legame nella direzione sbagliata in questo momento. OneWayToSource proverà ad aggiornare FlagIsModified nel contenitore ogni volta che IsModified cambia nel controllo che stai creando. Si desidera l'opposto, ovvero l'associazione IsModified a container.FlagIsModified. Per questo dovresti usare la modalità di rilegatura OneWay

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

Elenco completo dei membri dell'enumerazione: http://msdn.microsoft.com/en-us/library/system.windows.data.bindingmode.aspx


5
No, voglio esattamente lo scenario che descrivi che non voglio fare. FlagThingy.IsModified -> container.FlagIsModified
Inferis

3
Essere valutato perché l'interrogante aveva una domanda ambigua sembra un po 'eccessivo.
JaredPar

6
@ JaredPar: non vedo cosa c'è di ambiguo nella domanda. La domanda afferma che 1) esiste una proprietà di dipendenza di sola lettura IsIsModified, che 2) l'OP vuole dichiarare un'associazione su quella proprietà in XAML e che 3) l'associazione dovrebbe funzionare in OneWayToSourcemodalità. La tua soluzione non funziona praticamente perché, come descritto nella domanda, il compilatore non ti consente di dichiarare un'associazione su una proprietà di sola lettura e non funziona concettualmente perché IsModifiedè di sola lettura e quindi il suo valore non può essere modificato (dall'associazione).
OR Mapper
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.