我想编写一个ViewModel,它始终知道View中某些只读依赖项属性的当前状态。
具体来说,我的GUI包含一个FlowDocumentPageViewer,它一次从FlowDocument显示一个页面。 FlowDocumentPageViewer公开了两个名为CanGoToPreviousPage和CanGoToNextPage的只读依赖项属性。我希望我的ViewModel始终知道这两个View属性的值。
我想我可以用OneWayToSource数据绑定来完成这个:
<FlowDocumentPageViewer
CanGoToNextPage="{Binding NextPageAvailable, Mode=OneWayToSource}" ...>
如果允许,那将是完美的:每当FlowDocumentPageViewer的CanGoToNextPage属性发生更改时,新值将被推送到ViewModel的NextPageAvailable属性,这正是我想要的。
不幸的是,这不能编译:我收到错误,说'CanGoToPreviousPage'属性是只读的,无法通过标记设置。显然只读属性不支持任何类型的数据绑定,甚至不是与该属性相关的只读数据绑定。
我可以让我的ViewModel的属性为DependencyProperties,并使OneWay绑定以另一种方式运行,但我并不为关注点分离违反而疯狂(ViewModel需要对View的引用,MVVM数据绑定是应该避免)。
FlowDocumentPageViewer没有暴露CanGoToNextPageChanged事件,我不知道从DependencyProperty获取更改通知的任何好方法,没有创建另一个DependencyProperty来绑定它,这在这里看起来有点过分。
如何让我的ViewModel了解视图的只读属性的更改?
答案 0 :(得分:143)
是的,我过去使用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
的{{1}}事件。在句柄中,它会更新其FrameworkElement
和ObservedWidth
属性。因此,视图模型的ObservedHeight
和Width
始终与Height
的{{1}}和ActualWidth
同步。
也许不是完美的解决方案(我同意 - 只读DP 应该支持ActualHeight
绑定),但它可以工作并且它支持MVVM模式。显然,UserControl
和OneWayToSource
DP 不是只读。
更新:这是实现上述功能的代码:
ObservedWidth
答案 1 :(得分:55)
我使用的通用解决方案不仅适用于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();
}
}
答案 2 :(得分:20)
如果有其他人感兴趣,我在这里编写了肯特解决方案的近似值:
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
}
随意在您的应用中使用它。它运作良好。 (谢谢肯特!)
答案 3 :(得分:11)
这是我在这里写博客的这个“bug”的另一个解决方案:
OneWayToSource Binding for ReadOnly Dependency Property
它使用两个依赖属性,监听器和镜像。 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>
Download Demo Project Here。
它包含源代码和简短的样本用法,如果您对实现细节感兴趣,请访问my WPF blog。
最后一点,自.NET 4.0起,我们甚至远离内置支持,因为OneWayToSource Binding reads the value back from the Source after it has updated it
答案 4 :(得分:4)
我喜欢Dmitry Tashkinov的解决方案! 然而它在设计模式下撞毁了我的VS.这就是为什么我在OnSourceChanged方法中添加了一行:
private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!((bool)DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue)) ((DataPipe)d).OnSourceChanged(e); }
答案 5 :(得分: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);
}
}