Liaison OneWayToSource à partir de la propriété en lecture seule en XAML


87

J'essaie de me lier à une Readonlypropriété avec OneWayToSourcele mode as, mais il semble que cela ne puisse pas être fait en XAML:

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

Je reçois:

La propriété 'FlagThingy.IsModified' ne peut pas être définie car elle ne dispose pas d'un accesseur d'ensemble accessible.

IsModifiedest une lecture seule DependencyPropertysur FlagThingy. Je veux lier cette valeur à la FlagIsModifiedpropriété sur le conteneur.

Pour être clair:

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

Est-ce possible en utilisant uniquement XAML?


Mise à jour: Eh bien, j'ai résolu ce cas en définissant la liaison sur le conteneur et non sur le FlagThingy. Mais j'aimerais quand même savoir si c'est possible.


Mais comment définir la valeur d'une propriété en lecture seule?
idursun

3
Vous ne pouvez pas. Ce n'est pas non plus ce que j'essaie de réaliser. J'essaie d'obtenir de la propriété readonly IsModifiedà la propriété readwrite FlagIsModified.
Inferis

Bonne question. Votre solution de contournement ne fonctionne que si le conteneur est un DependencyObject et FlagIsModified est un DependencyProperty.
Josh G

10
Excellente question, mais je ne comprends pas la réponse acceptée. J'apprécierais si un gourou WPF pouvait m'éclairer un peu plus - Est-ce un bug ou par conception?
Oskar

@Oskar d'après cela, c'est un bug. aucune solution en vue cependant.
user1151923

Réponses:


45

Quelques résultats de recherche pour OneWayToSource ...

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

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

Option 3 (vraie propriété de dépendance en lecture seule)

System.ArgumentException: la propriété 'IsModified' ne peut pas être liée aux données.

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

Reflector donne la réponse:

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
C'est donc un bug, en fait.
Inferis

Belle recherche. Si vous ne l'aviez pas si bien présenté ici, j'aurais emprunté le même chemin douloureux. D'accord avec @Inferis.
kevinarpe du

1
Est-ce un bug? Pourquoi une liaison OneWayToSource ne serait-elle pas autorisée avec une DependencyProperty en lecture seule?
Alex Hope O'Connor le

Ce n'est pas un bug. C'est par conception et bien documenté. C'est à cause de la façon dont le moteur de liaison fonctionne en conjonction avec le système de propriétés de dépendance (la cible de liaison doit être un DependencyPropertyDP). Un DP en lecture seule ne peut être modifié qu'en utilisant le DependencyPropertyKey. Pour enregistrer un, BindingExpressionle moteur doit manipuler les métadonnées du DP cible. Comme il DependencyPropertyKeyest considéré comme privé pour garantir la protection publique en écriture, le moteur devra ignorer cette clé avec pour résultat de ne pas pouvoir enregistrer la liaison sur un DP en lecture seule.
BionicCode

23

Il s'agit d'une limitation de WPF et c'est par conception. Il est signalé sur Connect ici:
liaison OneWayToSource à partir d'une propriété de dépendance en lecture seule

J'ai fait une solution pour pouvoir pousser dynamiquement les propriétés de dépendance en lecture seule vers la source appelée sur PushBindinglaquelle j'ai blogué ici . L'exemple ci-dessous fait des OneWayToSourceliaisons à partir des DP en lecture seule ActualWidthet ActualHeightaux propriétés Largeur et Hauteur duDataContext

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

PushBindingfonctionne en utilisant deux propriétés de dépendance, écouteur et miroir. Listener est lié OneWayà TargetProperty et dans le PropertyChangedCallbackil met à jour la propriété Mirror qui est liée OneWayToSourceà tout ce qui a été spécifié dans la liaison.

Le projet de démonstration peut être téléchargé ici.
Il contient le code source et un court exemple d'utilisation.


Intéressant! Je suis venu avec une solution similaire et l'ai appelé un "Conduit" - le Conduit avait deux propriétés de dépendance selon votre conception et deux liaisons distinctes. Le cas d'utilisation que j'avais était de lier d'anciennes propriétés simples à d'anciennes propriétés simples en XAML.
Daniel Paull

3
Je vois que votre lien MS Connect ne fonctionne plus. Cela signifie-t-il que MS l'a corrigé dans une version plus récente de .NET ou qu'ils l'ont simplement supprimé?
Minuscule

@Tiny Connect semble avoir été finalement abandonné, malheureusement. Il était lié à de nombreux endroits. Je ne pense pas que cela implique spécifiquement de savoir si un problème a été résolu.
UuDdLrLrSs

J'étais sur le point d'écrire cette chose exacte. Bon travail!
aaronburro le

5

A écrit ceci:

Usage:

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

Code:

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

Je ne l'ai pas encore testé dans les styles et les modèles, je suppose qu'il a besoin d'un boîtier spécial.


2

Voici une autre solution de propriété attachée basée sur SizeObserver détaillée ici Repousser les propriétés d'interface graphique en lecture seule dans 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);
    }
}

Déclarer la propriété jointe dans le contrôle

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

1

Voici une autre implémentation pour la liaison à 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);
        }
    }
}

Utilisation en 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>

Cette implémentation est spécifique à la liaison Validation.HasError


0

WPF n'utilisera pas le setter de propriétés CLR, mais il semble qu'il effectue une validation étrange en fonction de celui-ci.

Peut-être dans votre situation cela peut être correct:

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

1
La propriété CLR n'est pas utilisée dans ce cas.
Inferis

Voulez-vous dire que vous venez de définir DependencyProperty et que vous avez pu écrire <controls: FlagThingy IsModified = "..." />? Pour moi, il dit: "La propriété 'IsModified' n'existe pas dans l'espace de noms XML" si je n'ajoute pas la propriété CLR.
alex2k8

1
Je crois que le moment du design utilise les propriétés clr alors que le runtime va directement à la propriété de dépendance (s'il en est une).
Meandmycode

La propriété CLR n'est pas nécessaire dans mon cas (je n'utilise pas IsModified à partir du code), mais elle est là quand même (avec seulement un setter public). Le designtime et le runtime fonctionnent correctement avec juste l'enregistrement de propriété de dépendance.
Inferis

La liaison elle-même n'utilise pas la propriété CLR, mais lorsque vous définissez la liaison en XAML, elle doit être traduite en code. Je suppose qu'à ce stade, l'analyseur XAML voit que la propriété IsModified est en lecture seule et lève une exception (même avant la création de la liaison).
alex2k8

0

Hmmm ... Je ne suis pas sûr d'être d'accord avec l'une de ces solutions. Que diriez-vous de spécifier un rappel de coercition dans votre enregistrement de propriété qui ignore les changements externes? Par exemple, j'avais besoin d'implémenter une propriété de dépendance Position en lecture seule pour obtenir la position d'un contrôle MediaElement dans un contrôle utilisateur. Voici comment je l'ai fait:

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

En d'autres termes, ignorez simplement la modification et renvoyez la valeur soutenue par un membre différent qui n'a pas de modificateur public. - Dans l'exemple ci-dessus, MediaRenderer est en fait le contrôle MediaElement privé.


Dommage que cela ne fonctionne pas pour les propriétés prédéfinies des classes BCL: - /
OR Mapper

0

La façon dont j'ai contourné cette limitation était d'exposer uniquement une propriété Binding dans ma classe, en gardant le DependencyProperty privé complètement. J'ai implémenté une propriété en écriture seule "PropertyBindingToSource" (celle-ci n'est pas une DependencyProperty) qui peut être définie sur une valeur de liaison dans le xaml. Dans le setter pour cette propriété en écriture seule, j'appelle à BindingOperations.SetBinding pour lier la liaison à DependencyProperty.

Pour l'exemple spécifique de l'OP, cela ressemblerait à ceci:

L'implémentation 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;
    }
}

Notez que l'objet DependencyProperty statique en lecture seule est privé. Dans le contrôle j'ai ajouté un bouton dont le clic est géré par Button_Click. L'utilisation du contrôle FlatThingy dans mon 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>

Notez que j'ai également implémenté un ViewModel pour la liaison à qui n'est pas montré ici. Il expose un DependencyProperty nommé "FlagIsModified" comme vous pouvez le glaner à partir de la source ci-dessus.

Cela fonctionne très bien, me permettant de repousser les informations dans le ViewModel à partir de la vue d'une manière faiblement couplée, avec la direction de ce flux d'informations explicitement définie.


-1

Vous faites la reliure dans la mauvaise direction en ce moment. OneWayToSource essaiera de mettre à jour FlagIsModified sur le conteneur chaque fois que IsModified change sur le contrôle que vous créez. Vous voulez le contraire, à savoir que IsModified se lie à container.FlagIsModified. Pour cela, vous devez utiliser le mode de liaison OneWay

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

Liste complète des membres de l'énumération: http://msdn.microsoft.com/en-us/library/system.windows.data.bindingmode.aspx


5
Non, je veux exactement le scénario que vous décrivez et que je ne veux pas faire. FlagThingy.IsModified -> container.FlagIsModified
Inferis

3
Être noté parce que l'interlocuteur avait une question ambiguë semble un peu exagéré.
JaredPar

6
@JaredPar: Je ne vois pas ce qui est ambigu dans la question. La question indique que 1) il existe une propriété de dépendance en lecture seule IsIsModified, que 2) l'OP souhaite déclarer une liaison sur cette propriété en XAML et que 3) la liaison est censée fonctionner en OneWayToSourcemode. Votre solution ne fonctionne pratiquement pas car, comme décrit dans la question, le compilateur ne vous laisse pas déclarer de liaison sur une propriété en lecture seule, et cela ne fonctionne pas conceptuellement car il IsModifiedest en lecture seule et donc sa valeur ne peut pas être changé (par la liaison).
OR Mapper
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.