C#WPF MVVM,附加了更新主窗口数据上下文的行为



简而言之:在MVVM模式中,访问主窗口数据上下文并通过行为类更新它是否正确?

long:我正在尝试学习WPF MVVM并制作应用程序,其中一个功能是用可拖动的省略号绘制画布。我发现了一些可以提供此功能的行为示例,但它们依赖于TranslateTransform,而这不是我想要的解决方案。我想提取椭圆坐标以供进一步使用
我还使用ItemsControl来显示画布和相关项目,这使得canvas.SetTop()命令无法使用。

经过几次尝试,我找到了一个可行的解决方案,但根据MVVM模式,我不确定这是否正确。如果这是实现目标的最简单方法……我把编码作为一种爱好如果我犯了一些概念错误,请告诉我。

简短应用程序描述:

  • 在应用程序启动时,TestWindow2VM类的实例被装入板条箱,并作为数据上下文分配给主窗口
  • TestWindow2VM类包含ObservableCollection,其中包含EllipseVM类
  • EllipseVM类保存X、Y坐标和其他一些数据(画笔等)
  • 在ItemsControl的XAML中,ItemsSource的绑定设置为我的ObservableCollection。在ItemsControl数据模板中,我将椭圆属性绑定到存储在EllipseVM类中的数据,并添加对我的行为类的引用
  • 在ItemsControl中ItemContainerStyle画布的顶部和左侧属性绑定到我的ObservableCollection
  • 当单击椭圆时,我的行为类访问数据上下文,找到EllipseVM类的实例,并根据鼠标光标相对于画布的位置更改X和Y坐标

下面的代码:

行为:

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有三种不同的对象:

  • 查看
  • VModel
  • 型号

视图的属性应该绑定到VModel,您可以像真正的专家一样,尝试正确地将视图与EllipseVM绑定!您的项目的问题是,您没有将单个视图绑定到单个VM,但您想要无限数量的VM模型。

我将在下面列举一些反思点:

  • 我想对您在不同拖动事件上注册的事实提出质疑:要素鼠标左键向下

只有在创建或销毁对象时才应该注册。

  • CanvasDragBehavior:为什么用静态公共属性(没有私有构造函数)实现类似singleton的模式?

  • 避免通过类似字符串的"来注册属性;CanBeDragged";找到定义和使用接口的方法,例如IMoveFeature{bool IsDraggable}

您的代码太复杂,并且有一些错误
例如,不需要CanvasDragBehavior的静态实例属性。看起来你搞混了什么。

要在Canvas上定位元素,只需使用附加的属性Canvas.TopCanvas.Left

首选输入事件的隧道版本,前缀为预览。例如,收听PreviewMouseMove而不是MouseMove

另一个重要的修复方法是使用WeakEventManager来订阅附加元素的事件。否则,您将创建潜在的内存泄漏(取决于事件发布者和事件侦听器的生存期)。请始终记住遵循以下模式以避免此类内存泄漏:当您订阅事件时,请确保始终取消订阅。如果无法控制对象的生存期,请始终遵循弱事件模式并使用WeakEventManager来观察事件
在您的情况下:当项目从ItemsControl.ItemsSource中删除时,您的行为将无法检测到此更改以取消订阅相应的事件
在您的上下文中,内存泄漏的风险并不高,但最好是安全的,而不是抱歉,并遵守安全模式。

在实现控件或行为时,尽量避免与数据类型和实现细节紧密耦合。使控件或行为尽可能通用。因此,您的行为不应该知道DataContext以及拖动的元素类型。通过这种方式,您可以简单地扩展代码或重用行为,例如,也可以拖动Rectangle。目前,您的代码仅适用于EllipseEllipseVM

通常,您不需要视图模型中的位置数据。如果是纯UI拖动&删除坐标只是视图的一部分。在这种情况下,您更愿意将行为附加到项容器,而不是将其附加到DataTemplate的元素:您不想拖动数据模型。您要拖动项目容器
如果您仍然需要模型中的坐标,您可以在ItemsControl.ItemContainerStyle中设置绑定,如下面的示例所示(项目容器StyleDataContext始终是数据项,在您的情况下为EllipseVM类型)。

目标是拖动项目容器而不是数据模型的简化和改进版本可以如下所示。请注意,以下行为是通过仅使用拖动对象的UIElement类型来实现的。根本不需要元素或数据模型的实际类型。这样,它将适用于每个形状或控件(不仅仅是Ellipse)。您甚至可以拖动ButtonDataTemplate中定义的任何内容。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);
}
}

用法示例

DataItem.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>

最新更新