wpf 将只读 GUI 属性推回 ViewModel
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/1083224/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Pushing read-only GUI properties back into ViewModel
提问by Joe White
I want to write a ViewModel that always knows the current state of some read-only dependency properties from the View.
我想编写一个 ViewModel,它始终知道 View 中某些只读依赖项属性的当前状态。
Specifically, my GUI contains a FlowDocumentPageViewer, which displays one page at a time from a FlowDocument. FlowDocumentPageViewer exposes two read-only dependency properties called CanGoToPreviousPage and CanGoToNextPage. I want my ViewModel to always know the values of these two View properties.
具体来说,我的 GUI 包含一个 FlowDocumentPageViewer,它一次显示一个 FlowDocument 中的一页。FlowDocumentPageViewer 公开了两个只读依赖属性,称为 CanGoToPreviousPage 和 CanGoToNextPage。我希望我的 ViewModel 始终知道这两个 View 属性的值。
I figured I could do this with a OneWayToSource databinding:
我想我可以用 OneWayToSource 数据绑定来做到这一点:
<FlowDocumentPageViewer
CanGoToNextPage="{Binding NextPageAvailable, Mode=OneWayToSource}" ...>
If this was allowed, it would be perfect: whenever the FlowDocumentPageViewer's CanGoToNextPage property changed, the new value would get pushed down into the ViewModel's NextPageAvailable property, which is exactly what I want.
如果允许这样做,那就太完美了:每当 FlowDocumentPageViewer 的 CanGoToNextPage 属性发生变化时,新值就会被推入 ViewModel 的 NextPageAvailable 属性中,这正是我想要的。
Unfortunately, this doesn't compile: I get an error saying 'CanGoToPreviousPage' property is read-only and cannot be set from markup.Apparently read-only properties don't support anykind of databinding, not even databinding that's read-only with respect to that property.
不幸的是,这无法编译:我收到一条错误消息,指出“CanGoToPreviousPage”属性是只读的,无法通过标记进行设置。显然只读属性不支持任何类型的数据绑定,甚至不支持对该属性只读的数据绑定。
I could make my ViewModel's properties be DependencyProperties, and make a OneWay binding going the other way, but I'm not crazy about the separation-of-concerns violation (ViewModel would need a reference to the View, which MVVM databinding is supposed to avoid).
我可以使我的 ViewModel 的属性成为 DependencyProperties,并以另一种方式进行 OneWay 绑定,但我对关注点分离的违规并不感到疯狂(ViewModel 需要对 View 的引用,MVVM 数据绑定应该避免这种情况) )。
FlowDocumentPageViewer doesn't expose a CanGoToNextPageChanged event, and I don't know of any good way to get change notifications from a DependencyProperty, short of creating another DependencyProperty to bind it to, which seems like overkill here.
FlowDocumentPageViewer 不公开 CanGoToNextPageChanged 事件,我不知道有什么好方法可以从 DependencyProperty 获取更改通知,除了创建另一个 DependencyProperty 来绑定它,这在这里似乎有点矫枉过正。
How can I keep my ViewModel informed of changes to the view's read-only properties?
如何让我的 ViewModel 了解视图只读属性的更改?
回答by Kent Boogaart
Yes, I've done this in the past with the ActualWidth
and ActualHeight
properties, both of which are read-only. I created an attached behavior that has ObservedWidth
and ObservedHeight
attached properties. It also has an Observe
property that is used to do the initial hook-up. Usage looks like this:
是的,我过去曾使用ActualWidth
和ActualHeight
属性完成此操作,这两个属性都是只读的。我创建了具有附加的行为ObservedWidth
和ObservedHeight
附加属性。它还具有Observe
用于进行初始连接的属性。用法如下所示:
<UserControl ...
SizeObserver.Observe="True"
SizeObserver.ObservedWidth="{Binding Width, Mode=OneWayToSource}"
SizeObserver.ObservedHeight="{Binding Height, Mode=OneWayToSource}"
So the view model has Width
and Height
properties that are always in sync with the ObservedWidth
and ObservedHeight
attached properties. The Observe
property simply attaches to the SizeChanged
event of the FrameworkElement
. In the handle, it updates its ObservedWidth
and ObservedHeight
properties. Ergo, the Width
and Height
of the view model is always in sync with the ActualWidth
and ActualHeight
of the UserControl
.
所以视图模型Width
和Height
特性是始终与同步ObservedWidth
和ObservedHeight
附加属性。该Observe
属性只是附加到 的SizeChanged
事件FrameworkElement
。在句柄中,它更新其ObservedWidth
和ObservedHeight
属性。因此,这个Width
和Height
视图模型总是同步与ActualWidth
和ActualHeight
的UserControl
。
Perhaps not the perfect solution (I agree - read-only DPs shouldsupport OneWayToSource
bindings), but it works and it upholds the MVVM pattern. Obviously, the ObservedWidth
and ObservedHeight
DPs are notread-only.
也许不是完美的解决方案(我同意 - 只读 DP应该支持OneWayToSource
绑定),但它有效并且支持 MVVM 模式。显然,ObservedWidth
和ObservedHeight
DP不是只读的。
UPDATE: here's code that implements the functionality described above:
更新:这是实现上述功能的代码:
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);
}
}
回答by Dmitry Tashkinov
I use a universal solution which works not only with ActualWidth and ActualHeight, but also with any data you can bind to at least in reading mode.
我使用了一种通用解决方案,它不仅适用于 ActualWidth 和 ActualHeight,而且适用于至少在阅读模式下可以绑定的任何数据。
The markup looks like this, provided ViewportWidth and ViewportHeight are properties of the view model
标记看起来像这样,前提是 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>
Here is the source code for the custom elements
这是自定义元素的源代码
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();
}
}
回答by Scott Whitlock
If anyone else is interested, I coded up an approximation of Kent's solution here:
如果其他人有兴趣,我在这里编写了 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
}
Feel free to use it in your apps. It works well. (Thanks Kent!)
随意在您的应用程序中使用它。它运作良好。(谢谢肯特!)
回答by Fredrik Hedblad
Here is another solution to this "bug" which I blogged about here:
OneWayToSource Binding for ReadOnly Dependency Property
这是我在这里写博客的这个“错误”的另一个解决方案:
OneWayToSource Binding for ReadOnly Dependency Property
It works by using two Dependency Properties, Listener and Mirror. Listener is bound OneWay to the TargetProperty and in the PropertyChangedCallback it updates the Mirror property which is bound OneWayToSource to whatever was specified in the Binding. I call it PushBinding
and it can be set on any read-only Dependency Property like this
它通过使用两个依赖属性,侦听器和镜像来工作。侦听器将 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>
Download Demo Project Here.
It contains source code and short sample usage, or visit my WPF blogif you're interested in the implementation details.
在此处下载演示项目。
它包含源代码和简短的示例用法,如果您对实现细节感兴趣,请访问我的 WPF 博客。
One last note, since .NET 4.0 we are even further away from built-in-support for this, since a OneWayToSource Binding reads the value back from the Source after it has updated it
最后一个注意事项,从 .NET 4.0 开始,我们离内置支持更远了,因为OneWayToSource 绑定在更新后从源读取值
回答by Dariusz Wasacz
I like Dmitry Tashkinov's solution! However it crashed my VS in design mode. That's why I added a line to OnSourceChanged method:
我喜欢 Dmitry Tashkinov 的解决方案!但是它在设计模式下使我的 VS 崩溃了。这就是为什么我在 OnSourceChanged 方法中添加了一行:
private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!((bool)DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue)) ((DataPipe)d).OnSourceChanged(e); }
回答by eriksmith200
I think it can be done a bit simpler:
我认为它可以做得更简单一点:
xaml:
xml:
behavior:ReadOnlyPropertyToModelBindingBehavior.ReadOnlyDependencyProperty="{Binding ActualWidth, RelativeSource={RelativeSource Self}}"
behavior:ReadOnlyPropertyToModelBindingBehavior.ModelProperty="{Binding MyViewModelProperty}"
cs:
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);
}
}