Возврат свойств GUI только для чтения в ViewModel
Я хочу написать ViewModel, который всегда знает текущее состояние некоторых свойств зависимостей только для чтения из представления.
в частности, мой графический интерфейс содержит FlowDocumentPageViewer, который отображает по одной странице за раз из FlowDocument. FlowDocumentPageViewer предоставляет два свойства зависимостей только для чтения, называемые CanGoToPreviousPage и CanGoToNextPage. Я хочу, чтобы мой ViewModel всегда знал значения этих двух свойств представления.
Я подумал, что могу сделать это с помощью в OneWayToSource привязка данных:
<FlowDocumentPageViewer
CanGoToNextPage="{Binding NextPageAvailable, Mode=OneWayToSource}" ...>
Если бы это было разрешено, это было бы идеально: всякий раз, когда свойство Cangotonextpage FlowDocumentPageViewer изменяется, новое значение будет вытеснено в свойство NextPageAvailable ViewModel, что именно то, что я хочу.
к сожалению, это не компилируется: я получаю сообщение об ошибке, говорящее 'CanGoToPreviousPage' свойство доступно только для чтения и не может быть установлено из разметки. видимо только для чтения свойства не поддержка любой вид привязки данных, даже не привязка данных, которая доступна только для чтения в отношении этого свойства.
Я мог бы сделать свойства моего ViewModel зависимыми свойствами и сделать одностороннюю привязку, идущую в другую сторону, но я не схожу с ума от нарушения разделения проблем (ViewModel потребуется ссылка на представление, которое MVVM databinding должен избегать).
FlowDocumentPageViewer не предоставляет событие CanGoToNextPageChanged, и я не знаю ни одного хорошего способа получить уведомления об изменениях от DependencyProperty, кроме создания другого DependencyProperty, чтобы привязать его, что кажется излишним здесь.
Как я могу информировать свою ViewModel об изменениях свойств представления только для чтения?
6 ответов:
Да, я делал это в прошлом с
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.возможно, не идеальное решение (я согласен-только для чтения DPs должны поддержка
OneWayToSourceпривязки), но он работает и поддерживает шаблон MVVM. Очевидно, чтоObservedWidthиObservedHeightДНС не только для чтения.обновление: вот код, который реализует функциональность, описанную выше:
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 для свойства зависимостей только для чтенияон работает с помощью двух свойств зависимостей, прослушивателя и зеркала. Прослушиватель привязан OneWay к TargetProperty и в PropertyChangedCallback он обновляет зеркальное свойство, которое привязано 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:
private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!((bool)DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue)) ((DataPipe)d).OnSourceChanged(e); }
Я думаю, что это можно сделать немного проще:
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); } }
Comments