[英]C# WPF MVVM, attached behavior to update mainwindow datacontext
In short: is it correct in MVVM pattern to access main window datacontext and update it through behavior class?简而言之:在 MVVM 模式中访问主 window 数据上下文并通过行为 class 更新它是否正确?
long: I'm trying to learn WPF MVVM and make app where one of the functionalities is canvas with draggable ellipses. long:我正在尝试学习 WPF MVVM 并制作其中一项功能为 canvas 且带有可拖动椭圆的应用程序。 I found few examples of behaviors that could provide this functionality but they relied on TranslateTransform and this was not the solution I wanted.
我发现很少有可以提供此功能的行为示例,但它们依赖于 TranslateTransform,这不是我想要的解决方案。 I want to extract the ellipse coordinates for furhter use.
我想提取椭圆坐标以供进一步使用。
I also use ItemsControl to display canvas and related items which made impossible to use Canvas.SetTop() command.我还使用 ItemsControl 来显示 canvas 和相关项目,这使得无法使用 Canvas.SetTop() 命令。
After several tries I found a working solution but I'm not sure if this is correct according to MVVM pattern.经过几次尝试,我找到了一个可行的解决方案,但我不确定根据 MVVM 模式这是否正确。 And if this is the simplest way to achieve the goal… I take up coding as a hobby if I made some concept mistakes please let me know.
如果这是实现目标的最简单方法……我将编码作为一种爱好,如果我犯了一些概念错误,请告诉我。
Short app description:简短的应用程序描述:
Code below:代码如下:
behavior:行为:
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: 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 distinct 3 kinds of object: MVVM 不同的 3 种 object:
The property of the view should be bound to the VModel, you try correctly to bind the view with EllipseVM, like a real Expert, The issue on your project is that you have not a single View bound to a single VM.视图的属性应该绑定到 VModel,您尝试正确地将视图与 EllipseVM 绑定,就像真正的专家一样,您项目的问题是您没有将单个视图绑定到单个 VM。 but you want an infinite number of VModels.
但你想要无限数量的 VModel。
I will enumerate below some points of reflection:我将在下面列举一些反思点:
you should register only when objects are created or destroyed.您应该仅在创建或销毁对象时注册。
CanvasDragBehavior: why do you implement a singleton like pattern with a static public property (without private constructor)? CanvasDragBehavior:为什么要使用 static 公共属性(没有私有构造函数)实现类似 singleton 的模式?
Avoid registering properties via string like "CanBeDragged" find a way to define and use interfaces eg IMoveFeature{bool IsDraggable}避免通过像“CanBeDragged”这样的字符串注册属性找到一种方法来定义和使用接口,例如 IMoveFeature{bool IsDraggable}
Your code is too complicated and has some errors.您的代码太复杂并且有一些错误。
For example the static instance property of the CanvasDragBehavior
is not required.例如,不需要 CanvasDragBehavior 的
CanvasDragBehavior
实例属性。 It looks like you confused something here.看起来你在这里混淆了一些东西。
To position the element on the Canvas
simply use the attached properties Canvas.Top
and Canvas.Left
.对于 position
Canvas
上的元素,只需使用附加属性Canvas.Top
和Canvas.Left
。
Prefer the tunneling version of input events, prefixed with Preview .首选输入事件的隧道版本,前缀为Preview 。 For example listen to
PreviewMouseMove
instead of MouseMove
.例如,听
PreviewMouseMove
而不是MouseMove
。
Another important fix is to use the WeakEventManager
to subscribe to the events of the attached elements.另一个重要的修复是使用
WeakEventManager
订阅附加元素的事件。 Otherwise you create a potential memory leak (depending on the lifetime of the event publisher and event listener).否则,您会造成潜在的 memory 泄漏(取决于事件发布者和事件侦听器的生命周期)。 Always remember to follow the following pattern to avoid such memory leaks: when you subscribe to events, ensure that you will always unsubscribe too.
始终记住遵循以下模式以避免此类 memory 泄漏:当您订阅事件时,请确保您也将始终取消订阅。 If you have no control over the object lifetime, always follow the Weak Event pattern and use the
WeakEventManager
to observe events.如果您无法控制 object 生命周期,请始终遵循弱事件模式并使用 Wea
WeakEventManager
来观察事件。
In your case: when an item is removed from the ItemsControl.ItemsSource
, your behavior won't be able to detect this change in order to unsubscribe from the corresponding events.在您的情况下:当从
ItemsControl.ItemsSource
中删除一个项目时,您的行为将无法检测到此更改以取消订阅相应的事件。
The risk of a memory leak in your context is not high, but better be safe than sorry and stick to the safety pattern.在您的上下文中发生 memory 泄漏的风险并不高,但安全总比后悔好,并坚持安全模式。
When implementing a control or behavior, try to avoid tight coupling to data types and implementation details.在实现控件或行为时,尽量避免与数据类型和实现细节紧密耦合。 Make the control or behavior as generic as possible.
使控件或行为尽可能通用。 For this reason, your behavior should not know about the
DataContext
and what type of elements are dragged.因此,您的行为不应了解
DataContext
以及拖动的元素类型。 This way you can simply extend your code or reuse the behavior for example to allow to drag a Rectangle
too.这样您就可以简单地扩展您的代码或重用该行为,例如也允许拖动一个
Rectangle
。 Right now, your code only works with a Ellipse
or EllipseVM
.现在,您的代码仅适用于
Ellipse
或EllipseVM
。
Usually, you don't need the position data in your view model. If it's pure UI drag&Drop the coordinates are part of the view only.通常,您不需要视图 model 中的 position 数据。如果是纯 UI 拖放,则坐标只是视图的一部分。 In this case you would prefer to attach the behavior to the item container instead of attaching it to the elements of the
DataTemplate
: you don't want to drag the data model. You want to drag the item container.在这种情况下,您更愿意将行为附加到项目容器而不是将其附加到
DataTemplate
的元素:您不想拖动数据 model。您想要拖动项目容器。
If you still need the coordinates in your model, you would setup a binding in the ItemsControl.ItemContainerStyle
like in the example below (the DataContext
of the item container Style
is always the data item, which is of type EllipseVM
in your case).如果您仍然需要 model 中的坐标,您可以在
ItemsControl.ItemContainerStyle
中设置绑定,如下例所示(项目容器Style
的DataContext
始终是数据项,在您的情况下为EllipseVM
类型)。
The simplified and improved version that targets dragging the item container rather than the data model could look as follows.针对拖动项目容器而不是数据 model 的简化和改进版本如下所示。 Note that the following behavior is implemented by only using the
UIElement
type for the dragged object. The actual type of the element or the data model is not required at all.请注意,以下行为是通过仅使用拖动的 object 的
UIElement
类型来实现的。根本不需要元素的实际类型或数据 model。 This way it will work with every shape or control (not only Ellipse
).这样它将适用于每个形状或控件(不仅是
Ellipse
)。 You can even drag a Button
or whatever is defined in the DataTemplate
.您甚至可以拖动
Button
或DataTemplate
中定义的任何内容。 The DataContext
can be any type. 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数据项.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));
}
MainWindow.xaml主窗口.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.