[英]C# WPF MVVM, attached behavior to update mainwindow datacontext
简而言之:在 MVVM 模式中访问主 window 数据上下文并通过行为 class 更新它是否正确?
long:我正在尝试学习 WPF MVVM 并制作其中一项功能为 canvas 且带有可拖动椭圆的应用程序。 我发现很少有可以提供此功能的行为示例,但它们依赖于 TranslateTransform,这不是我想要的解决方案。 我想提取椭圆坐标以供进一步使用。
我还使用 ItemsControl 来显示 canvas 和相关项目,这使得无法使用 Canvas.SetTop() 命令。
经过几次尝试,我找到了一个可行的解决方案,但我不确定根据 MVVM 模式这是否正确。 如果这是实现目标的最简单方法……我将编码作为一种爱好,如果我犯了一些概念错误,请告诉我。
简短的应用程序描述:
代码如下:
行为:
public class CanvasDragBehavior
{
private Point _mouseCurrentPos;
private Point _mouseStartOffset;
private bool _dragged;
private static CanvasDragBehavior _dragBehavior = new CanvasDragBehavior();
public static CanvasDragBehavior dragBehavior
{
get { return _dragBehavior; }
set { _dragBehavior = value; }
}
public static readonly DependencyProperty IsDragProperty =
DependencyProperty.RegisterAttached("CanBeDragged",
typeof(bool), typeof(DragBehavior),
new PropertyMetadata(false, OnDragChanged));
public static bool GetCanBeDragged(DependencyObject obj)
{
return (bool)obj.GetValue(IsDragProperty);
}
public static void SetCanBeDragged(DependencyObject obj, bool value)
{
obj.SetValue(IsDragProperty, value);
}
private static void OnDragChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var element = (UIElement)sender;
var isDrag = (bool)(e.NewValue);
dragBehavior = new CanvasDragBehavior();
if (isDrag)
{
element.MouseLeftButtonDown += dragBehavior.ElementOnMouseLeftButtonDown;
element.MouseLeftButtonUp += dragBehavior.ElementOnMouseLeftButtonUp;
element.MouseMove += dragBehavior.ElementOnMouseMove;
}
else
{
element.MouseLeftButtonDown -= dragBehavior.ElementOnMouseLeftButtonDown;
element.MouseLeftButtonUp -= dragBehavior.ElementOnMouseLeftButtonUp;
element.MouseMove -= dragBehavior.ElementOnMouseMove;
}
}
private void ElementOnMouseMove(object sender, MouseEventArgs e)
{
if (!_dragged) return;
Canvas canvas = Extension.FindAncestor<Canvas>(((FrameworkElement)sender));
if (canvas != null)
{
_mouseCurrentPos = e.GetPosition(canvas);
FrameworkElement fe = (FrameworkElement)sender;
if (fe.DataContext.GetType() == typeof(EllipseVM))
{
// EllipseVM class contains X and Y coordinates that are used in ItemsControl to display the ellipse
EllipseVM ellipseVM = (EllipseVM)fe.DataContext;
double positionLeft = _mouseCurrentPos.X - _mouseStartOffset.X;
double positionTop = _mouseCurrentPos.Y - _mouseStartOffset.Y;
#region canvas border check
if (positionLeft < 0) positionLeft = 0;
if (positionTop < 0) positionTop = 0;
if (positionLeft > canvas.ActualWidth) positionLeft = canvas.ActualWidth-fe.Width;
if (positionTop > canvas.ActualHeight) positionTop = canvas.ActualHeight-fe.Height;
#endregion
ellipseVM.left = positionLeft;
ellipseVM.top = positionTop;
}
}
}
private void ElementOnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_mouseStartOffset = e.GetPosition((FrameworkElement)sender);
_dragged = true;
((UIElement)sender).CaptureMouse();
}
private void ElementOnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_dragged = false;
((UIElement)sender).ReleaseMouseCapture();
}
XAML:
<ItemsControl ItemsSource="{Binding scrollViewElements}" >
<ItemsControl.Resources>
<!--some other data templates here-->
</DataTemplate>
<DataTemplate DataType="{x:Type VM:EllipseVM}" >
<Ellipse Width="{Binding width}"
Height="{Binding height}"
Fill="{Binding fillBrush}"
Stroke="Red" StrokeThickness="1"
behaviors:CanvasDragBehavior.CanBeDragged="True"
/>
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="Transparent" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Top" Value="{Binding top}"/>
<Setter Property="Canvas.Left" Value="{Binding left}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
MVVM 不同的 3 种 object:
视图的属性应该绑定到 VModel,您尝试正确地将视图与 EllipseVM 绑定,就像真正的专家一样,您项目的问题是您没有将单个视图绑定到单个 VM。 但你想要无限数量的 VModel。
我将在下面列举一些反思点:
您应该仅在创建或销毁对象时注册。
CanvasDragBehavior:为什么要使用 static 公共属性(没有私有构造函数)实现类似 singleton 的模式?
避免通过像“CanBeDragged”这样的字符串注册属性找到一种方法来定义和使用接口,例如 IMoveFeature{bool IsDraggable}
您的代码太复杂并且有一些错误。
例如,不需要 CanvasDragBehavior 的CanvasDragBehavior
实例属性。 看起来你在这里混淆了一些东西。
对于 position Canvas
上的元素,只需使用附加属性Canvas.Top
和Canvas.Left
。
首选输入事件的隧道版本,前缀为Preview 。 例如,听PreviewMouseMove
而不是MouseMove
。
另一个重要的修复是使用WeakEventManager
订阅附加元素的事件。 否则,您会造成潜在的 memory 泄漏(取决于事件发布者和事件侦听器的生命周期)。 始终记住遵循以下模式以避免此类 memory 泄漏:当您订阅事件时,请确保您也将始终取消订阅。 如果您无法控制 object 生命周期,请始终遵循弱事件模式并使用 Wea WeakEventManager
来观察事件。
在您的情况下:当从ItemsControl.ItemsSource
中删除一个项目时,您的行为将无法检测到此更改以取消订阅相应的事件。
在您的上下文中发生 memory 泄漏的风险并不高,但安全总比后悔好,并坚持安全模式。
在实现控件或行为时,尽量避免与数据类型和实现细节紧密耦合。 使控件或行为尽可能通用。 因此,您的行为不应了解DataContext
以及拖动的元素类型。 这样您就可以简单地扩展您的代码或重用该行为,例如也允许拖动一个Rectangle
。 现在,您的代码仅适用于Ellipse
或EllipseVM
。
通常,您不需要视图 model 中的 position 数据。如果是纯 UI 拖放,则坐标只是视图的一部分。 在这种情况下,您更愿意将行为附加到项目容器而不是将其附加到DataTemplate
的元素:您不想拖动数据 model。您想要拖动项目容器。
如果您仍然需要 model 中的坐标,您可以在ItemsControl.ItemContainerStyle
中设置绑定,如下例所示(项目容器Style
的DataContext
始终是数据项,在您的情况下为EllipseVM
类型)。
针对拖动项目容器而不是数据 model 的简化和改进版本如下所示。 请注意,以下行为是通过仅使用拖动的 object 的UIElement
类型来实现的。根本不需要元素的实际类型或数据 model。 这样它将适用于每个形状或控件(不仅是Ellipse
)。 您甚至可以拖动Button
或DataTemplate
中定义的任何内容。 DataContext
可以是任何类型。
public class CanvasDragBehavior
{
public static readonly DependencyProperty IsDragEnabledProperty = DependencyProperty.RegisterAttached(
"IsDragEnabled",
typeof(bool),
typeof(CanvasDragBehavior),
new PropertyMetadata(false, OnIsDragEnabledChanged));
public static bool GetIsDragEnabled(DependencyObject obj) => (bool)obj.GetValue(IsDragEnabledProperty);
public static void SetIsDragEnabled(DependencyObject obj, bool value) => obj.SetValue(IsDragEnabledProperty, value);
private static Point DragStartPosition { get; set; }
private static ConditionalWeakTable<UIElement, FrameworkElement> ItemToItemHostMap { get; } = new ConditionalWeakTable<UIElement, FrameworkElement>();
private static void OnIsDragEnabledChanged(object attachingElement, DependencyPropertyChangedEventArgs e)
{
if (attachingElement is not UIElement uiElement)
{
return;
}
var isEnabled = (bool)e.NewValue;
if (isEnabled)
{
WeakEventManager<UIElement, MouseButtonEventArgs>.AddHandler(uiElement, nameof(uiElement.PreviewMouseLeftButtonDown), OnDraggablePreviewMouseLeftButtonDown);
WeakEventManager<UIElement, MouseEventArgs>.AddHandler(uiElement, nameof(uiElement.PreviewMouseMove), OnDraggablePreviewMouseMove);
WeakEventManager<UIElement, MouseButtonEventArgs>.AddHandler(uiElement, nameof(uiElement.PreviewMouseLeftButtonUp), OnDraggablePreviewMouseLeftButtonUp);
}
else
{
WeakEventManager<UIElement, MouseButtonEventArgs>.RemoveHandler(uiElement, nameof(uiElement.PreviewMouseLeftButtonDown), OnDraggablePreviewMouseLeftButtonDown);
WeakEventManager<UIElement, MouseEventArgs>.RemoveHandler(uiElement, nameof(uiElement.PreviewMouseMove), OnDraggablePreviewMouseMove);
WeakEventManager<UIElement, MouseButtonEventArgs>.RemoveHandler(uiElement, nameof(uiElement.PreviewMouseLeftButtonUp), OnDraggablePreviewMouseLeftButtonUp);
}
}
private static void OnDraggablePreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Released)
{
return;
}
var draggable = sender as UIElement;
if (!ItemToItemHostMap.TryGetValue(draggable, out FrameworkElement draggableHost))
{
return;
}
Point newDragEndPosition = e.GetPosition(draggableHost);
newDragEndPosition.Offset(-DragStartPosition.X, -DragStartPosition.Y);
Canvas.SetLeft(draggable, newDragEndPosition.X);
Canvas.SetTop(draggable, newDragEndPosition.Y);
}
private static void OnDraggablePreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var draggable = sender as UIElement;
if (!ItemToItemHostMap.TryGetValue(draggable, out _))
{
if (!TryGetVisualParent(draggable, out Panel draggableHost))
{
return;
}
ItemToItemHostMap.Add(draggable, draggableHost);
}
DragStartPosition = e.GetPosition(draggable);
draggable.CaptureMouse();
}
private static void OnDraggablePreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
=> (sender as UIElement)?.ReleaseMouseCapture();
private static bool TryGetVisualParent<TParent>(DependencyObject element, out TParent parent) where TParent : DependencyObject
{
parent = null;
if (element is null)
{
return false;
}
element = VisualTreeHelper.GetParent(element);
if (element is TParent parentElement)
{
parent = parentElement;
return true;
}
return TryGetVisualParent(element, out parent);
}
}
数据项.cs
class DataItem : INotifyPropertyChanged
{
// Allow this item to change its coordinates aka get dragged
private bool isPositionDynamic;
public bool IsPositionDynamic
{
get => this.isPositionDynamic;
set
{
this.isPositionDynamic = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
主窗口.xaml
<Window>
<ItemsControl ItemsSource="{Binding DataItems}"
Height="1000"
Width="1000">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type local:DataItem}">
<Ellipse Width="50"
Height="50"
Fill="Red"
Stroke="Black"
StrokeThickness="1" />
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Width="{Binding RelativeSource={RelativeSource AncestorType=ItemsControl}, Path=ActualWidth}"
Height="{Binding RelativeSource={RelativeSource AncestorType=ItemsControl}, Path=ActualHeight}"
Background="Gray" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<!-- If every item is draggable, simply set this property to 'True' -->
<Setter Property="local:CanvasDragBehavior.IsDragEnabled"
Value="{Binding IsPositionDynamic}" />
<!-- Optional binding if you need the coordinates in the view model.
This example assumes that the view model has a Top and Left property -->
<Setter Property="Canvas.Top"
Value="{Binding Top, Mode=TwoWay}" />
<Setter Property="Canvas.Left"
Value="{Binding Left, Mode=TwoWay}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</Window>
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.