Нажатие свойств GUI только для чтения обратно в ViewModel

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

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

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

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

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

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

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

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

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

Ответ 1

Да, я делал это в прошлом с параметрами 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. Ergo, модель Width и Height модели просмотра всегда синхронизируется с ActualWidth и ActualHeight UserControl.

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

UPDATE: здесь код, реализующий описанную выше функциональность:

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

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

Ответ 3

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

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
}

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

Ответ 4

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

Он работает с использованием двух свойств зависимостей, прослушивателя и зеркала. Listener привязано OneWay к TargetProperty и в PropertyChangedCallback обновляет свойство Mirror, которое привязано OneWayToSource к тому, что было указано в Binding. Я называю его 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 Binding считывает значение из Источника после его обновления это

Ответ 5

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

    private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!((bool)DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue))
            ((DataPipe)d).OnSourceChanged(e);
    }

Ответ 6

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

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