将只读 GUI 属性推回 ViewModel

我想编写一个 ViewModel,它总是知道 View 中某些只读依赖属性的当前状态。

具体来说,我的 GUI 包含一个 FlowDocumentPageViewer,它一次显示来自 FlowDocument 的一个页面。FlowDocumentPageViewer 公开名为 CanGoToPreviousPage 和 CanGoToNextPage 的两个只读依赖属性。我希望我的 ViewModel 始终知道这两个 View 属性的值。

我想我可以用 OneWayToSource 数据绑定来做到这一点:

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

如果这是允许的,那将是完美的: 每当 FlowDocumentPageViewer 的 CanGoToNextPage 属性发生变化时,新的值将被下推到 ViewModel 的 NextPage 属性中,这正是我想要的。

不幸的是,这不能编译: 我得到一个错误说 “ CanGoToPreviousPage”属性是只读的,不能从标记设置。显然只读属性不支持 任何类型的数据绑定,甚至不支持相对于该属性的只读数据绑定。

我可以将 ViewModel 的属性设置为 DependencyProperties,并将 OneWay 绑定设置为相反的方向,但是我并不热衷于关注点分离(ViewModel 需要一个对 View 的引用,而 MVVM 数据绑定应该避免这一点)。

FlowDocumentPageViewer 不会公开 CanGoToNextPageChanged 事件,而且我不知道有什么好办法可以从 DependencyProperty 获得更改通知,除非创建另一个 DependencyProperty 来绑定它,这在这里似乎有点过了。

如何让 ViewModel 随时了解视图只读属性的更改?

47279 次浏览

是的,我过去曾经使用 ActualWidthActualHeight属性这样做过,这两个属性都是只读的。我创建了一个具有 ObservedWidthObservedHeight附加属性的附加行为。它还有一个 Observe属性,用于执行初始挂接。用法如下:

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

因此,视图模型具有 WidthHeight属性,这些属性总是与 ObservedWidthObservedHeight附加属性同步。Observe属性只是附加到 FrameworkElementSizeChanged事件。在句柄中,它更新其 ObservedWidthObservedHeight属性。因此,视图模型的 WidthHeight总是与 Height3的 Height1和 Height2同步。

也许不是完美的解决方案(我同意只读的 DP 应该支持 OneWayToSource绑定) ,但是它可以工作,并且支持 MVVM 模式。显然,ObservedWidthObservedHeight 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);
}
}

如果还有人感兴趣的话,我在这里编写了一个 Kent 解的近似值:

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
}

请随意在你的应用程序中使用它。它工作得很好。(感谢肯特!)

我使用一个通用的解决方案,它不仅工作的实际宽度和实际高度,而且与任何数据,你可以绑定到至少在阅读模式。

如果 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();
}
}

下面是我在博客中提到的解决这个“ bug”的另一个方法:
只读依赖属性的 OneWayToSource 绑定

它使用两个依赖项属性: 监听器和镜像。监听器将 OneWay 绑定到 TargetProperty,并在 PropertyChangedCallback 中更新将 OneWayToSource 绑定到 Binding 中指定的任何内容的 Mirror 属性。我称它为 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>

在这里下载演示项目。
它包含源代码和简短的示例用法。

最后要注意的是,自从.NET 4.0以来,我们离内置支持更远了,因为有了 OneWayToSource 绑定在源更新该值后从源读取该值

我喜欢 Dmitry Tashkinov 的解决方案! 但是它在设计模式下崩溃了我的 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}"

译者:

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