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

Я хочу написать 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 для его привязки, что здесь выглядит избыточным.

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

7 ответов

Решение

Да, я делал это в прошлом с 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 а также ObservedHeight DP не доступны только для чтения.

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

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

Я использую универсальное решение, которое работает не только с 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();
    }
}

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

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
}

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

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

Он работает с использованием двух свойств зависимости, Listener и Mirror. Слушатель привязан 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 считывает значение из источника после его обновления.

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

    закрытая статическая пустота OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!((bool)DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue))
            ((DataPipe) г).OnSourceChanged(е);
    }

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

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 — это можно сделать гораздо проще с помощьюBehavior, без ограничения количества свойств, ниже приведен примерActualHeightиActualWidth:

xaml:

      <Window x:Class="WpfApp21.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:Behaviors="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:local="clr-namespace:WpfApp21"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="MainWindow"
        Width="378"
        Height="293"
        d:DataContext="{d:DesignInstance Type=local:Model}"
        mc:Ignorable="d">
    <Behaviors:Interaction.Behaviors>
        <local:ReadOnlyPropertyToModelBindingBehavior ModelProperty="{Binding Height}" ReadOnlyDependencyProperty="{Binding ActualHeight, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}" />
        <local:ReadOnlyPropertyToModelBindingBehavior ModelProperty="{Binding Width}" ReadOnlyDependencyProperty="{Binding ActualWidth, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}" />
    </Behaviors:Interaction.Behaviors>
    <Grid>
        <TextBlock Margin="35,38,0,0"
                   HorizontalAlignment="Left"
                   VerticalAlignment="Top"
                   Text="{Binding Height}"
                   TextWrapping="Wrap">
            
        </TextBlock>
        <TextBlock Margin="35,103,0,0"
                   HorizontalAlignment="Left"
                   VerticalAlignment="Top"
                   Text="{Binding Width}"
                   TextWrapping="Wrap" />
    </Grid>
</Window>

КС:

      using Microsoft.Xaml.Behaviors;
using System.ComponentModel;
using System.Windows;

namespace WpfApp21
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new Model();
        }
    }

    public class Model : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        private double _Height;
        public double Height
        {
            get { return _Height; }
            set
            {
                if (value == _Height) return;
                _Height = value;
                OnPropertyChanged(nameof(Height));
            }
        }

        private double _Width;
        public double Width
        {
            get { return _Width; }
            set
            {
                if (value == _Width) return;
                _Width = value;
                OnPropertyChanged(nameof(Width));
            }
        }
    }

    public class ReadOnlyPropertyToModelBindingBehavior : Behavior<UIElement>
    {
        public object ReadOnlyDependencyProperty
        {
            get { return (object)GetValue(ReadOnlyDependencyPropertyProperty); }
            set { SetValue(ReadOnlyDependencyPropertyProperty, value); }
        }

        public static readonly DependencyProperty ReadOnlyDependencyPropertyProperty =
            DependencyProperty.Register("ReadOnlyDependencyProperty", typeof(object), typeof(ReadOnlyPropertyToModelBindingBehavior),
                new PropertyMetadata(null, OnReadOnlyDependencyPropertyPropertyChanged));

        public object ModelProperty
        {
            get { return (object)GetValue(ModelPropertyProperty); }
            set { SetValue(ModelPropertyProperty, value); }
        }

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

        private static void OnReadOnlyDependencyPropertyPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var b = obj as ReadOnlyPropertyToModelBindingBehavior;
            b.ModelProperty = e.NewValue;
        }
    }
}

Другие вопросы по тегам