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

发布于 2025-01-19 12:08:23 字数 5992 浏览 2 评论 0 原文

简而言之: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>

In short: is it correct in MVVM pattern to access main window datacontext and update it through behavior class?

long: I'm trying to learn WPF MVVM and make app where one of the functionalities is canvas with draggable ellipses. I found few examples of behaviors that could provide this functionality but they relied on TranslateTransform and this was not the solution I wanted. 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.

After several tries I found a working solution but I’m not sure if this is correct according to MVVM pattern. 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:

  • On app startup the instance of TestWindow2VM class is crated and assigned to main window as datacontext
  • TestWindow2VM class contains ObservableCollection which contains EllipseVM class.
  • EllipseVM class holds X,Y coordinates and some other data (brushes etc).
  • In XAML in ItemsControl the binding of ItemsSource is set to my ObservableCollection. In ItemsControl Datatemplate I bind ellipse properties to data stored in EllipseVM class and also add reference to my behavior class
  • in ItemsControl ItemContainerStyle canvas top and left properties are bound to my ObservableCollection
  • when ellipse is clicked my behavior class access the datacontext, finds the instance of EllipseVM class and changes X and Y coordinates basing on mouse cursor position relative to canvas.

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:

<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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

静水深流 2025-01-26 12:08:23

MVVM不同的3种对象:

  • 视图
  • VModel
  • 模型

该视图的属性应绑定到VModel,您可以正确地尝试使用EllipSevm绑定视图,就像真实的专家一样!
项目的问题是,您没有单个视图绑定到单个VM,但是您需要无限数量的VModels。

我将列举以下一些反思点:

  • 我想挑战您在不同的拖放事件上注册的事实:
    element.mouseleftbuttondown

您只有在创建或破坏对象时才会注册。

  • canvasdragbehavior:为什么要使用静态公共财产(没有私有构造函数)实现单身模式?

  • 避免通过字符串注册属性,例如“ canbedragged”找到一种定义和使用接口的方法

MVVM distinct 3 kinds of object:

  • View
  • VModel
  • Model

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:

  • I would like to challenge the fact that you register on different drag events:
    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}

野稚 2025-01-26 12:08:23

您的代码太复杂了,有一些错误。
例如,不需要 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 可以是任何类型。

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));
}

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

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 properties Canvas.Top and Canvas.Left.

Prefer the tunneling version of input events, prefixed with Preview. For example listen to PreviewMouseMove instead of MouseMove.

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 the WeakEventManager 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 a Rectangle too. Right now, your code only works with a Ellipse or 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. 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 (the DataContext of the item container Style is always the data item, which is of type EllipseVM 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 only Ellipse). You can even drag a Button or whatever is defined in the DataTemplate. The DataContext can be any type.

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);
  }
}

Usage example

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));
}

MainWindow.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>
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文