Отправка свойств графического интерфейса только для чтения обратно в ViewModel

124

Я хочу написать ViewModel, который всегда знает текущее состояние некоторых свойств зависимостей только для чтения из View.

В частности, мой графический интерфейс содержит FlowDocumentPageViewer, который отображает по одной странице из FlowDocument. FlowDocumentPageViewer предоставляет два свойства зависимостей, доступных только для чтения, которые называются CanGoToPreviousPage и CanGoToNextPage. Я хочу, чтобы моя ViewModel всегда знала значения этих двух свойств View.

Я подумал, что могу сделать это с привязкой данных OneWayToSource:

<FlowDocumentPageViewer
    CanGoToNextPage="{Binding NextPageAvailable, Mode=OneWayToSource}" ...>

Если бы это было разрешено, это было бы идеально: всякий раз, когда свойство CanGoToNextPage в FlowDocumentPageViewer изменялось, новое значение передавалось в свойство NextPageAvailable ViewModel, а это именно то, что я хочу.

К сожалению, это не компилируется: я получаю сообщение об ошибке, что свойство CanGoToPreviousPage доступно только для чтения и не может быть установлено из разметки. Очевидно, свойства, доступные только для чтения, не поддерживают никакую привязку данных, даже привязку данных, доступную только для чтения в отношении этого свойства.

Я мог бы сделать свои свойства ViewModel DependencyProperties и сделать привязку OneWay в другую сторону, но я не в восторге от нарушения разделения проблем (ViewModel потребуется ссылка на View, которого привязка данных MVVM должна избегать ).

FlowDocumentPageViewer не предоставляет событие CanGoToNextPageChanged, и я не знаю ни одного хорошего способа получать уведомления об изменениях из DependencyProperty, кроме создания другого DependencyProperty для его привязки, что здесь кажется излишним.

Как я могу информировать мою модель представления об изменениях свойств представления, доступных только для чтения?

Джо Уайт
источник

Ответы:

152

Да, я сделал это в прошлом , с ActualWidthи ActualHeightсвойствами, оба из которых являются только для чтения. Я создал прикрепленное поведение, у которого есть ObservedWidthи ObservedHeightприкрепленные свойства. У него также есть Observeсвойство, которое используется для первоначального подключения. Использование выглядит так:

<UserControl ...
    SizeObserver.Observe="True"
    SizeObserver.ObservedWidth="{Binding Width, Mode=OneWayToSource}"
    SizeObserver.ObservedHeight="{Binding Height, Mode=OneWayToSource}"

Таким образом, модель имеет вид Widthи Heightсвойства , которые всегда находятся в синхронизации с ObservedWidthи ObservedHeightприсоединенными свойствами. ObserveСвойство просто придает SizeChangedсобытию FrameworkElement. В ручке, он обновляет ObservedWidthи ObservedHeightсвойства. Следовательно, то Widthи Heightв модели представления всегда синхронизирован с ActualWidthи ActualHeightиз UserControl.

Возможно, это не идеальное решение (я согласен - DP только для чтения должны поддерживать OneWayToSourceпривязки), но оно работает и поддерживает шаблон MVVM. Очевидно, что ObservedWidthи ObservedHeightDP не доступны только для чтения.

ОБНОВЛЕНИЕ: вот код, реализующий описанные выше функции:

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

    public static readonly DependencyProperty ObservedWidthProperty = DependencyProperty.RegisterAttached(
        "ObservedWidth",
        typeof(double),
        typeof(SizeObserver));

    public static readonly DependencyProperty ObservedHeightProperty = DependencyProperty.RegisterAttached(
        "ObservedHeight",
        typeof(double),
        typeof(SizeObserver));

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

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

    public static double GetObservedWidth(FrameworkElement frameworkElement)
    {
        frameworkElement.AssertNotNull("frameworkElement");
        return (double)frameworkElement.GetValue(ObservedWidthProperty);
    }

    public static void SetObservedWidth(FrameworkElement frameworkElement, double observedWidth)
    {
        frameworkElement.AssertNotNull("frameworkElement");
        frameworkElement.SetValue(ObservedWidthProperty, observedWidth);
    }

    public static double GetObservedHeight(FrameworkElement frameworkElement)
    {
        frameworkElement.AssertNotNull("frameworkElement");
        return (double)frameworkElement.GetValue(ObservedHeightProperty);
    }

    public static void SetObservedHeight(FrameworkElement frameworkElement, double observedHeight)
    {
        frameworkElement.AssertNotNull("frameworkElement");
        frameworkElement.SetValue(ObservedHeightProperty, observedHeight);
    }

    private static void OnObserveChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        var frameworkElement = (FrameworkElement)dependencyObject;

        if ((bool)e.NewValue)
        {
            frameworkElement.SizeChanged += OnFrameworkElementSizeChanged;
            UpdateObservedSizesForFrameworkElement(frameworkElement);
        }
        else
        {
            frameworkElement.SizeChanged -= OnFrameworkElementSizeChanged;
        }
    }

    private static void OnFrameworkElementSizeChanged(object sender, SizeChangedEventArgs e)
    {
        UpdateObservedSizesForFrameworkElement((FrameworkElement)sender);
    }

    private static void UpdateObservedSizesForFrameworkElement(FrameworkElement frameworkElement)
    {
        // WPF 4.0 onwards
        frameworkElement.SetCurrentValue(ObservedWidthProperty, frameworkElement.ActualWidth);
        frameworkElement.SetCurrentValue(ObservedHeightProperty, frameworkElement.ActualHeight);

        // WPF 3.5 and prior
        ////SetObservedWidth(frameworkElement, frameworkElement.ActualWidth);
        ////SetObservedHeight(frameworkElement, frameworkElement.ActualHeight);
    }
}
Кент Богаарт
источник
2
Интересно, можно ли проделать какие-нибудь уловки, чтобы автоматически прикрепить свойства без необходимости Observe. Но это похоже на прекрасное решение. Спасибо!
Джо Уайт
1
Спасибо, Кент. Ниже я разместил образец кода для этого класса «SizeObserver».
Скотт Уитлок,
52
+1 к этому мнению: «DP только для чтения должны поддерживать привязки OneWayToSource»
Тристан
3
Возможно, даже лучше создать всего одно Sizeсвойство, сочетая высоту и ширину. Прибл. На 50% меньше кода.
Джерард
1
@Gerard: Это не сработает, потому что здесь нет ActualSizeсобственности FrameworkElement. Если вы хотите напрямую привязать прикрепленные свойства, вы должны создать два свойства, которые будут привязаны к ActualWidthи, ActualHeightсоответственно.
dotNET
59

Я использую универсальное решение, которое работает не только с ActualWidth и ActualHeight, но и с любыми данными, к которым вы можете привязаться хотя бы в режиме чтения.

Разметка выглядит так, если ViewportWidth и ViewportHeight являются свойствами модели представления.

<Canvas>
    <u:DataPiping.DataPipes>
         <u:DataPipeCollection>
             <u:DataPipe Source="{Binding RelativeSource={RelativeSource AncestorType={x:Type Canvas}}, Path=ActualWidth}"
                         Target="{Binding Path=ViewportWidth, Mode=OneWayToSource}"/>
             <u:DataPipe Source="{Binding RelativeSource={RelativeSource AncestorType={x:Type Canvas}}, Path=ActualHeight}"
                         Target="{Binding Path=ViewportHeight, Mode=OneWayToSource}"/>
          </u:DataPipeCollection>
     </u:DataPiping.DataPipes>
<Canvas>

Вот исходный код для настраиваемых элементов

public class DataPiping
{
    #region DataPipes (Attached DependencyProperty)

    public static readonly DependencyProperty DataPipesProperty =
        DependencyProperty.RegisterAttached("DataPipes",
        typeof(DataPipeCollection),
        typeof(DataPiping),
        new UIPropertyMetadata(null));

    public static void SetDataPipes(DependencyObject o, DataPipeCollection value)
    {
        o.SetValue(DataPipesProperty, value);
    }

    public static DataPipeCollection GetDataPipes(DependencyObject o)
    {
        return (DataPipeCollection)o.GetValue(DataPipesProperty);
    }

    #endregion
}

public class DataPipeCollection : FreezableCollection<DataPipe>
{

}

public class DataPipe : Freezable
{
    #region Source (DependencyProperty)

    public object Source
    {
        get { return (object)GetValue(SourceProperty); }
        set { SetValue(SourceProperty, value); }
    }
    public static readonly DependencyProperty SourceProperty =
        DependencyProperty.Register("Source", typeof(object), typeof(DataPipe),
        new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnSourceChanged)));

    private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((DataPipe)d).OnSourceChanged(e);
    }

    protected virtual void OnSourceChanged(DependencyPropertyChangedEventArgs e)
    {
        Target = e.NewValue;
    }

    #endregion

    #region Target (DependencyProperty)

    public object Target
    {
        get { return (object)GetValue(TargetProperty); }
        set { SetValue(TargetProperty, value); }
    }
    public static readonly DependencyProperty TargetProperty =
        DependencyProperty.Register("Target", typeof(object), typeof(DataPipe),
        new FrameworkPropertyMetadata(null));

    #endregion

    protected override Freezable CreateInstanceCore()
    {
        return new DataPipe();
    }
}
Дмитрий Ташкинов
источник
(из ответа user543564): Это не ответ, а комментарий Дмитрию - я использовал ваше решение, и оно отлично сработало. Хорошее универсальное решение, которое можно универсально использовать в разных местах. Я использовал его, чтобы вставить некоторые свойства элемента пользовательского интерфейса (ActualHeight и ActualWidth) в мою модель просмотра.
Марк Гравелл
2
Спасибо! Это помогло мне привязаться к обычному свойству только для получения. К сожалению, свойство не публиковало события INotifyPropertyChanged. Я решил эту проблему, присвоив имя привязке DataPipe и добавив следующее к событию изменения элементов управления: BindingOperations.GetBindingExpressionBase (bindingName, DataPipe.SourceProperty) .UpdateTarget ();
chilltemp
3
Это решение сработало для меня. Моя единственная настройка заключалась в том, чтобы установить BindsTwoWayByDefault в значение true для FrameworkPropertyMetadata в TargetProperty DependencyProperty.
Hasani Blackwell
1
Единственная претензия к этому решению, по-видимому, заключается в том, что оно нарушает чистую инкапсуляцию, поскольку Targetсвойство необходимо сделать доступным для записи, хотя его нельзя изменять извне: - /
ИЛИ Mapper
Для тех, кто предпочитает пакет NuGet копированию и вставке кода: я добавил DataPipe в свою библиотеку JungleControls с открытым исходным кодом. См. Документацию DataPipe .
Роберт Важан
21

Если кому-то еще интересно, я закодировал здесь приближение решения Кента:

class SizeObserver
{
    #region " Observe "

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

    public static void SetObserve(
      FrameworkElement elem, bool value)
    {
        elem.SetValue(ObserveProperty, value);
    }

    public static readonly DependencyProperty ObserveProperty =
        DependencyProperty.RegisterAttached("Observe", typeof(bool), typeof(SizeObserver),
        new UIPropertyMetadata(false, OnObserveChanged));

    static void OnObserveChanged(
      DependencyObject depObj, DependencyPropertyChangedEventArgs e)
    {
        FrameworkElement elem = depObj as FrameworkElement;
        if (elem == null)
            return;

        if (e.NewValue is bool == false)
            return;

        if ((bool)e.NewValue)
            elem.SizeChanged += OnSizeChanged;
        else
            elem.SizeChanged -= OnSizeChanged;
    }

    static void OnSizeChanged(object sender, RoutedEventArgs e)
    {
        if (!Object.ReferenceEquals(sender, e.OriginalSource))
            return;

        FrameworkElement elem = e.OriginalSource as FrameworkElement;
        if (elem != null)
        {
            SetObservedWidth(elem, elem.ActualWidth);
            SetObservedHeight(elem, elem.ActualHeight);
        }
    }

    #endregion

    #region " ObservedWidth "

    public static double GetObservedWidth(DependencyObject obj)
    {
        return (double)obj.GetValue(ObservedWidthProperty);
    }

    public static void SetObservedWidth(DependencyObject obj, double value)
    {
        obj.SetValue(ObservedWidthProperty, value);
    }

    // Using a DependencyProperty as the backing store for ObservedWidth.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ObservedWidthProperty =
        DependencyProperty.RegisterAttached("ObservedWidth", typeof(double), typeof(SizeObserver), new UIPropertyMetadata(0.0));

    #endregion

    #region " ObservedHeight "

    public static double GetObservedHeight(DependencyObject obj)
    {
        return (double)obj.GetValue(ObservedHeightProperty);
    }

    public static void SetObservedHeight(DependencyObject obj, double value)
    {
        obj.SetValue(ObservedHeightProperty, value);
    }

    // Using a DependencyProperty as the backing store for ObservedHeight.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ObservedHeightProperty =
        DependencyProperty.RegisterAttached("ObservedHeight", typeof(double), typeof(SizeObserver), new UIPropertyMetadata(0.0));

    #endregion
}

Не стесняйтесь использовать его в своих приложениях. Это работает хорошо. (Спасибо, Кент!)

Скотт Уитлок
источник
10

Вот еще одно решение этой «ошибки», о которой я писал здесь:
Привязка OneWayToSource для свойства зависимости ReadOnly

Он работает с использованием двух свойств зависимостей, прослушивателя и зеркала. Слушатель привязан OneWay к TargetProperty, а в PropertyChangedCallback он обновляет свойство Mirror, которое привязано OneWayToSource к тому, что было указано в привязке. Я называю его, PushBindingи его можно установить для любого свойства зависимостей только для чтения, например этого

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

Загрузите демонстрационный проект здесь .
Он содержит исходный код и краткий пример использования, или посетите мой блог WPF, если вас интересуют детали реализации.

И последнее замечание: начиная с .NET 4.0 мы еще дальше отошли от встроенной поддержки для этого, поскольку привязка OneWayToSource считывает значение обратно из источника после его обновления.

Фредрик Хедблад
источник
Ответы на Stack Overflow должны быть полностью автономными. Можно включить ссылку на необязательные внешние ссылки, но весь код, необходимый для ответа, должен быть включен в сам ответ. Обновите свой вопрос, чтобы его можно было использовать, не посещая другие веб-сайты.
Питер Дунихо
4

Мне нравится решение Дмитрия Ташкинова! Однако он разбил мой VS в режиме проектирования. Поэтому я добавил в метод OnSourceChanged строку:

    private static void OnSourceChanged (DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        если (! ((bool) DesignerProperties.IsInDesignModeProperty.GetMetadata (typeof (DependencyObject)). DefaultValue))
            ((DataPipe) г) .OnSourceChanged (е);
    }
Дариуш Васач
источник
0

Я думаю, что это можно сделать немного проще:

XAML:

behavior:ReadOnlyPropertyToModelBindingBehavior.ReadOnlyDependencyProperty="{Binding ActualWidth, RelativeSource={RelativeSource Self}}"
behavior:ReadOnlyPropertyToModelBindingBehavior.ModelProperty="{Binding MyViewModelProperty}"

CS:

public class ReadOnlyPropertyToModelBindingBehavior
{
  public static readonly DependencyProperty ReadOnlyDependencyPropertyProperty = DependencyProperty.RegisterAttached(
     "ReadOnlyDependencyProperty", 
     typeof(object), 
     typeof(ReadOnlyPropertyToModelBindingBehavior),
     new PropertyMetadata(OnReadOnlyDependencyPropertyPropertyChanged));

  public static void SetReadOnlyDependencyProperty(DependencyObject element, object value)
  {
     element.SetValue(ReadOnlyDependencyPropertyProperty, value);
  }

  public static object GetReadOnlyDependencyProperty(DependencyObject element)
  {
     return element.GetValue(ReadOnlyDependencyPropertyProperty);
  }

  private static void OnReadOnlyDependencyPropertyPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
  {
     SetModelProperty(obj, e.NewValue);
  }


  public static readonly DependencyProperty ModelPropertyProperty = DependencyProperty.RegisterAttached(
     "ModelProperty", 
     typeof(object), 
     typeof(ReadOnlyPropertyToModelBindingBehavior), 
     new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

  public static void SetModelProperty(DependencyObject element, object value)
  {
     element.SetValue(ModelPropertyProperty, value);
  }

  public static object GetModelProperty(DependencyObject element)
  {
     return element.GetValue(ModelPropertyProperty);
  }
}
eriksmith200
источник
2
Может быть немного проще, но если я хорошо его прочитал, он допускает только одну такую ​​привязку к элементу. Я имею в виду, я думаю, что при таком подходе вы не сможете привязать одновременно ActualWidth и ActualHeight. Только один из них.
quetzalcoatl