C# WPF MVVM,附加行为更新主窗口数据上下文
简而言之:MVVM 模式中访问主窗口数据上下文并通过行为类更新它是否正确?
long:我正在尝试学习 WPF MVVM 并制作应用程序,其中功能之一是带有可拖动椭圆的画布。我发现很少有可以提供此功能的行为示例,但它们依赖于 TranslateTransform,这不是我想要的解决方案。我想提取椭圆坐标以供进一步使用。
我还使用 ItemsControl 显示画布和相关项目,这使得无法使用 Canvas.SetTop() 命令。
经过几次尝试,我找到了一个可行的解决方案,但我不确定根据 MVVM 模式这是否正确。如果这是实现目标的最简单方法……我将编码作为一种爱好 如果我犯了一些概念错误,请告诉我。
简短的应用程序描述:
- 在应用程序启动时,TestWindow2VM 类的实例被创建并分配给主窗口,因为数据上下文
- TestWindow2VM 类包含 ObservableCollection,其中包含 EllipseVM 类。
- EllipseVM 类保存 X、Y 坐标和一些其他数据(画笔等)。
- 在 ItemsControl 的 XAML 中,ItemsSource 的绑定设置为我的 ObservableCollection。在 ItemsControl Datatemplate 中,我将椭圆属性绑定到 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>
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
MVVM不同的3种对象:
该视图的属性应绑定到VModel,您可以正确地尝试使用EllipSevm绑定视图,就像真实的专家一样!
项目的问题是,您没有单个视图绑定到单个VM,但是您需要无限数量的VModels。
我将列举以下一些反思点:
element.mouseleftbuttondown
您只有在创建或破坏对象时才会注册。
canvasdragbehavior:为什么要使用静态公共财产(没有私有构造函数)实现单身模式?
?
避免通过字符串注册属性,例如“ canbedragged”找到一种定义和使用接口的方法
MVVM distinct 3 kinds of 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, but you want an infinite number of VModels.
I will enumerate below some points of reflection:
element.MouseLeftButtonDown
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) ?
Avoid registering properties via string like "CanBeDragged" find a way to define and use interfaces eg IMoveFeature{bool IsDraggable}
您的代码太复杂了,有一些错误。
例如,不需要
canvasdragbehavior
的静态实例属性。看起来您在这里混淆了一些东西。要将元素放在
canvas
上,只需使用附加属性canvas.top
和canvas.left
。优先使用 preview 前缀的输入事件的隧道版本。例如,请听
PreviewMouseMove
而不是Mousemove
。另一个重要的解决方法是使用
weakeventmanager
订阅所附元素的事件。否则,您会创建潜在的内存泄漏(取决于事件发布者和事件侦听器的寿命)。始终记住要遵循以下模式以避免这种内存泄漏:当您订阅事件时,请确保您也会始终退订。如果您无法控制对象寿命,请始终按照弱事件模式并使用weakeventmanager
观察事件。在您的情况下:从
itemscontrol.itemssource
中删除项目时,您的行为将无法检测到此更改以便从相应的事件中取消订阅。在您的上下文中,内存泄漏的风险不是很高,但是要安全比后悔更好,并坚持安全模式。
实施控制或行为时,请尝试避免与数据类型和实现细节紧密耦合。使控制或行为尽可能通用。因此,您的行为不应知道
datacontext
以及拖动哪种类型的元素。这样,您可以简单地扩展代码或重复使用该行为,例如允许拖动矩形
。现在,您的代码仅与椭圆
或eLLIPSEVM
一起使用。通常,您不需要查看模型中的位置数据。如果纯UI拖动&amp;放下坐标仅是视图的一部分。在这种情况下,您希望将行为附加到项目容器上,而不是将其附加到
dataTemplate
的元素上:您不想拖动数据模型。您想拖动项目容器。如果您仍然需要模型中的坐标,则可以在
itemscontrol.itemcontainerstyle
中设置一个绑定,如下示例(项目容器的dataContext
始终是数据项,在您的情况下是类型ellipsevm
)。针对拖动项目容器而不是数据模型的简化和改进的版本可以如下。请注意,以下行为仅通过仅使用拖动对象的
uielement
类型来实现。根本不需要元素或数据模型的实际类型。这样,它将与每个形状或控件一起使用(不仅椭圆
)。您甚至可以拖动按钮
或datatemplate
中定义的内容。datacontext
可以是任何类型。用法示例
dataitem.cs
mainwindow.xaml
Your code is too complicated and has some errors.
For example the static instance property of the
CanvasDragBehavior
is not required. It looks like you confused something here.To position the element on the
Canvas
simply use the attached propertiesCanvas.Top
andCanvas.Left
.Prefer the tunneling version of input events, prefixed with Preview. For example listen to
PreviewMouseMove
instead ofMouseMove
.Another important fix is to use the
WeakEventManager
to subscribe to the events of the attached elements. Otherwise you create a potential memory leak (depending on the lifetime of the event publisher and event listener). Always remember to follow the following pattern to avoid such memory leaks: when you subscribe to events, ensure that you will always unsubscribe too. If you have no control over the object lifetime, always follow the Weak Event pattern and use theWeakEventManager
to observe events.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.The risk of a memory leak in your context is not high, but better be safe than sorry and stick to the safety pattern.
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. This way you can simply extend your code or reuse the behavior for example to allow to drag aRectangle
too. Right now, your code only works with aEllipse
orEllipseVM
.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. 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.If you still need the coordinates in your model, you would setup a binding in the
ItemsControl.ItemContainerStyle
like in the example below (theDataContext
of the item containerStyle
is always the data item, which is of typeEllipseVM
in your case).The simplified and improved version that targets dragging the item container rather than the data model could look as follows. 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. This way it will work with every shape or control (not onlyEllipse
). You can even drag aButton
or whatever is defined in theDataTemplate
. TheDataContext
can be any type.Usage example
DataItem.cs
MainWindow.xaml