Прив'язка OneWayToSource до властивості лише для читання в XAML


87

Я намагаюся прив'язати до Readonlyвластивості з OneWayToSourceрежимом as, але, схоже, цього неможливо зробити в XAML:

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

Я отримав:

Властивість 'FlagThingy.IsModified' неможливо встановити, оскільки вона не має доступного набору доступу.

IsModifiedце тільки для читання DependencyPropertyна FlagThingy. Я хочу прив'язати це значення до FlagIsModifiedвластивості контейнера.

Щоб бути зрозумілим:

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

Чи можливо це за допомогою лише XAML?


Оновлення: Ну, я виправив цей випадок, встановивши прив'язку до контейнера, а не до FlagThingy. Але я все одно хотів би знати, чи можливо це.


Але як можна встановити значення властивості лише для читання?
idursun

3
Ви не можете. Це також не те, що я намагаюся досягти. Я намагаюся отримати властивість FROM readonly для властивості IsModifiedreadwrite FlagIsModified.
Inferis

Хороше питання. Ваше обхідне рішення працює, лише якщо контейнер є DependencyObject, а FlagIsModified - DependencyProperty.
Josh G

10
Чудове питання, проте я не розумію прийняту відповідь. Я був би вдячний, якби якийсь гуру WPF міг би ще більше просвітити мене - це помилка чи дизайн?
Оскар

@Oskar відповідно до цього це помилка. жодного виправлення в полі зору, хоча.
user1151923

Відповіді:


46

Деякі результати досліджень для OneWayToSource ...

Варіант No1.

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

Варіант No2

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

Варіант №3 (Властивість справжньої залежності лише для читання)

System.ArgumentException: Властивість 'IsModified' не може бути прив'язана до даних.

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

Рефлектор дає відповідь:

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
Отже, це помилка, насправді.
Inferis

Гарне дослідження. Якби ви не так добре виклали це тут, я пройшов би той самий болючий шлях. Погодьтеся з @Inferis.
кевінарпе

1
Це помилка? Чому прив'язка OneWayToSource не допускається до властивості DependencyProperty лише для читання?
Алекс Хоуп О'Коннор,

Це не помилка. Це за проектом і добре задокументовано. Це пов’язано з тим, як механізм прив’язки працює разом із системою властивостей залежностей (ціллю прив’язки має бути DependencyPropertyDP). DP лише для читання може бути змінений лише за допомогою відповідного DependencyPropertyKey. Для реєстрації BindingExpressionдвигуна необхідно маніпулювати метаданими цільового DP. Оскільки DependencyPropertyKeyвважається приватним, щоб гарантувати захист від публічного запису, двигун повинен буде проігнорувати цей ключ, в результаті чого не вдалося зареєструвати прив'язку на DP, доступному лише для читання.
BionicCode

23

Це обмеження WPF, і це за проектом. Про це повідомляється на Connect тут:
прив’язка OneWayToSource із властивості залежності лише для читання

Я створив рішення для динамічної можливості передавати властивості залежності лише для читання до джерела, PushBindingяке називається, про що я писав тут . Нижче приклад робить OneWayToSourceприв'язки з тільки для читання ДПА ActualWidthі ActualHeightдо властивостей ширини і висотDataContext

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

PushBindingпрацює за допомогою двох властивостей залежності, прослуховувача та дзеркала. Слухач прив’язаний OneWayдо властивості TargetProperty і в PropertyChangedCallbackньому оновлюється властивість Mirror, яка прив’язана OneWayToSourceдо того, що було вказано в Binding.

Демо-проект можна завантажити тут.
Він містить вихідний код та короткий зразок використання.


Цікаво! Я придумав подібне рішення і назвав його "трубопровід" - він мав дві властивості залежності відповідно до вашої конструкції та два окремих прив’язки. Я використовував приклад використання прив'язки простих старих властивостей до простих старих властивостей у XAML.
Daniel Paull,

3
Я бачу, що ваше посилання MS Connect більше не працює. Це означає, що MS виправила це в новій версії .NET або вони просто видалили?
Крихітний

Здається, від @Tiny Connect врешті-решт відмовились, на жаль. Це було пов’язано з багатьма місцями. Я не думаю, що це конкретно передбачає щось про те, чи була виправлена ​​проблема.
UuDdLrLrSs

Я збирався написати саме цю річ. Гарна робота!
aaronburro

5

Написав це:

Використання:

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

Код:

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

Ще не тестували його у стилях та шаблонах, здогадуйтесь, що він потребує спеціального корпусу.


2

Ось ще одне вкладене рішення властивостей, засноване на SizeObserver, докладно описане тут. Відновлення властивостей графічного інтерфейсу лише для читання у 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);
    }
}

Заявіть вкладене майно під контроль

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

1

Ось ще одна реалізація для прив’язки до 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);
        }
    }
}

Використання в 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>

Ця реалізація є специфічною для прив'язки Validation.HasError


0

WPF не використовуватиме засіб встановлення властивостей CLR, але, здається, робить певну дивну перевірку на його основі.

Можливо, у вашій ситуації це може бути нормально:

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

1
Властивість CLR у цьому випадку не використовується.
Inferis

Ви маєте на увазі, що ви щойно визначили DependencyProperty і змогли написати <control: FlagThingy IsModified = "..." />? Для мене це говорить: "Властивість 'IsModified' не існує в просторі імен XML", якщо я не додаю властивість CLR.
alex2k8

1
Я вважаю, що час проектування використовує властивості clr, де як час виконання насправді переходить безпосередньо до властивості залежності (якщо вона одна).
meandmycode

Властивість CLR в моєму випадку непотрібна (я не використовую IsModified з коду), але тим не менше вона є (лише із загальнодоступним сетером). Як час виконання, так і час роботи чудово працюють лише з реєстрацією властивостей залежностей.
Inferis

Само прив'язка не використовує властивість CLR, але коли ви визначаєте прив'язку в XAML, її потрібно перевести в код. Я думаю, на цьому етапі аналізатор XAML бачить, що властивість IsModified є лише для читання та видає виняток (навіть до створення прив'язки).
alex2k8

0

Хм-м-м ... Я не впевнений, що погоджуюсь з будь-яким із цих рішень. Як щодо вказівки зворотного виклику примусу при реєстрації власності, який ігнорує зовнішні зміни? Наприклад, мені потрібно було реалізувати властивість залежності Позиція лише для читання, щоб отримати позицію елемента керування MediaElement всередині елемента керування користувача. Ось як я це зробив:

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

Іншими словами, просто проігноруйте зміну і поверніть значення, підтримане іншим членом, який не має загальнодоступного модифікатора. - У наведеному вище прикладі MediaRenderer насправді є приватним елементом управління MediaElement.


Шкода, що це не працює для заздалегідь визначених властивостей класів BCL: - /
АБО Mapper

0

Я працював над цим обмеженням, щоб виставляти лише властивість Binding у своєму класі, зберігаючи DependencyProperty взагалі приватним. Я реалізував властивість лише для запису "PropertyBindingToSource" (це не DependencyProperty), якому можна встановити значення прив'язки в xaml. У сеттері для цієї властивості лише для запису я викликаю BindingOperations.SetBinding, щоб зв'язати прив'язку до властивості DependencyProperty.

Для конкретного прикладу OP це може виглядати так:

Реалізація 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;
    }
}

Зверніть увагу, що статичний об’єкт DependencyProperty лише для читання є приватним. У елемент керування я додав кнопку, натисканням якої обробляє Button_Click. Використання елемента керування FlatThingy у моєму 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>

Зверніть увагу, що я також застосував ViewModel для прив'язки до цього, що тут не показано. Він відкриває властивість DependencyProperty з назвою "FlagIsModified", як ви можете отримати з джерела вище.

Це чудово працює, дозволяючи мені слабко пов’язати інформацію з View в ViewModel, з чітко визначеним напрямком цього інформаційного потоку.


-1

Зараз ви робите прив’язку в неправильному напрямку. OneWayToSource спробує оновити FlagIsModified на контейнері, коли IsModified зміниться на елементі керування, який ви створюєте. Вам потрібно навпаки, тобто прив’язка IsModified до контейнера.FlagIsModified. Для цього слід використовувати режим прив'язки OneWay

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

Повний список членів переліку: http://msdn.microsoft.com/en-us/library/system.windows.data.bindingmode.aspx


5
Ні, я хочу саме той сценарій, який ви описуєте, а я не хочу робити. FlagThingy.IsModified -> контейнер.FlagIsModified
Inferis

3
Означення, оскільки запитувальник мав неабияке запитання, здається трохи надмірним.
JaredPar

6
@JaredPar: Я не бачу, що неоднозначне в цьому питанні. Питання стверджує, що 1) існує властивість залежності лише для читання IsIsModified, що 2) OP хоче оголосити прив'язку до цього властивості в XAML і що 3) прив'язка повинна працювати в OneWayToSourceрежимі. Ваше рішення практично не працює, оскільки, як описано у питанні, компілятор не дозволяє оголосити прив'язку властивості лише для читання, а також не працює концептуально, оскільки IsModifiedє лише для читання, і, отже, його значення не може бути змінено (за палітуркою).
АБО Mapper
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.