Привязка 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. Но я все же хотел бы знать, возможно ли это.

Inferis
источник
Но как установить значение свойства только для чтения?
idursun
3
Вы не можете. Это тоже не то, чего я пытаюсь достичь. Я пытаюсь получить свойство FROM readonly для свойства IsModifiedreadwrite FlagIsModified.
Inferis,
Хороший вопрос. Ваш обходной путь работает, только если контейнер является DependencyObject, а FlagIsModified - DependencyProperty.
Джош Джи,
10
Отличный вопрос, однако я не понимаю принятого ответа. Я был бы признателен, если бы какой-нибудь гуру WPF просветил меня еще немного - это ошибка или дизайн?
Оскар,
@Oskar согласно этому это ошибка. но исправления не видно.
user1151923

Ответы:

46

Некоторые результаты исследования OneWayToSource ...

Опция 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);

Вариант # 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}" />

Вариант № 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");
    }
 ....
alex2k8
источник
30
Так что на самом деле это ошибка.
Inferis,
Хорошее исследование. Если бы ты не выложил это здесь так красиво, я бы пошел тем же болезненным путем. Согласитесь с @Inferis.
kevinarpe
1
Это ошибка? Почему нельзя использовать привязку OneWayToSource с доступным только для чтения DependencyProperty?
Alex Hope O'Connor
Это не ошибка. Это задумано и хорошо задокументировано. Это связано с тем, как механизм привязки работает вместе с системой свойств зависимостей (целью привязки должен быть DependencyPropertyDP). DP только для чтения может быть изменен только с помощью связанного DependencyPropertyKey. Чтобы зарегистрировать, BindingExpressionдвижок должен манипулировать метаданными целевого DP. Поскольку DependencyPropertyKeyон считается закрытым, чтобы гарантировать общедоступную защиту от записи, движок должен будет игнорировать этот ключ, что приведет к невозможности зарегистрировать привязку на DP, доступном только для чтения.
BionicCode
23

Это ограничение WPF, и это сделано специально. Об этом сообщается в Connect здесь:
привязка OneWayToSource из свойства зависимости только для чтения

Я сделал решение, позволяющее динамически передавать свойства зависимостей, доступные только для чтения, в вызываемый источник, о PushBindingкотором я писал здесь . В приведенном ниже примере выполняются OneWayToSourceпривязки из DP, доступных только для чтения, ActualWidthи ActualHeightк свойствам Width и Height объекта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к тому, что было указано в привязке.

Демо-проект можно скачать здесь.
Он содержит исходный код и небольшой пример использования.

Фредрик Хедблад
источник
Интересно! Я придумал аналогичное решение и назвал его «Conduit» - у Conduit было два свойства зависимости в соответствии с вашим дизайном и две отдельные привязки. В моем случае использовалась привязка простых старых свойств к простым старым свойствам в XAML.
Daniel Paull
3
Я вижу, что ваша ссылка MS Connect больше не работает. Означает ли это, что MS исправила это в более новой версии .NET или просто удалила?
Tiny
К сожалению, от @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}">    
jv_
источник
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"); }
    }
alex2k8
источник
1
Свойство CLR в этом случае не используется.
Inferis,
Вы имеете в виду, что вы только что определили DependencyProperty и смогли написать <controls: FlagThingy IsModified = "..." />? Для меня он говорит: «Свойство IsModified не существует в пространстве имен XML», если я не добавляю свойство CLR.
alex2k8
1
Я считаю, что время разработки использует свойства clr, тогда как время выполнения фактически переходит непосредственно к свойству зависимости (если оно есть).
meandmycode
Свойство CLR в моем случае не нужно (я не использую IsModified из кода), но тем не менее оно есть (только с общедоступным сеттером). И среда разработки, и среда выполнения работают нормально только с регистрацией свойства зависимостей.
Inferis,
Сама привязка не использует свойство CLR, но когда вы определяете привязку в XAML, ее необходимо транслировать в код. Я предполагаю, что на этом этапе синтаксический анализатор XAML видит, что свойство IsModified доступно только для чтения, и выдает исключение (даже до создания привязки).
alex2k8
0

Хммм ... Я не уверен, что согласен ни с одним из этих решений. Как насчет указания обратного вызова принуждения в вашей регистрации собственности, который игнорирует внешние изменения? Например, мне нужно было реализовать свойство зависимости Position, доступное только для чтения, чтобы получить положение элемента управления 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: - /
OR 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», как вы можете понять из приведенного выше источника.

Он отлично работает, позволяя мне возвращать информацию обратно в ViewModel из View в слабосвязанной манере с явным определением направления этого информационного потока.

Джон Тойтс
источник
-1

Вы сейчас делаете переплет в неправильном направлении. OneWayToSource будет пытаться обновить FlagIsModified в контейнере всякий раз, когда IsModified изменяет элемент управления, который вы создаете. Вам нужно обратное, а именно привязку IsModified к container.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 -> container.FlagIsModified
Inferis
3
Быть отмеченным из-за двусмысленного вопроса, который задает вопрос, кажется излишним.
JaredPar
6
@JaredPar: Я не вижу двусмысленности в этом вопросе. В вопросе говорится, что 1) существует свойство зависимости только для чтения IsIsModified, 2) OP хочет объявить привязку к этому свойству в XAML и 3) привязка должна работать в этом OneWayToSourceрежиме. Ваше решение практически не работает, потому что, как описано в вопросе, компилятор не позволит вам объявить привязку к свойству, IsModifiedдоступному только для чтения, и оно не работает концептуально, потому что доступно только для чтения и, следовательно, его значение не может быть изменено (по привязке).
OR Mapper