简体   繁体   English

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

[英]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:简短的应用程序描述:

  • On app startup the instance of TestWindow2VM class is crated and assigned to main window as datacontext在应用程序启动时,TestWindow2VM class 的实例被创建并分配给 main window 作为数据上下文
  • TestWindow2VM class contains ObservableCollection which contains EllipseVM class. TestWindow2VM class 包含 ObservableCollection,其中包含 EllipseVM class。
  • EllipseVM class holds X,Y coordinates and some other data (brushes etc). EllipseVM class 包含 X、Y 坐标和一些其他数据(画笔等)。
  • In XAML in ItemsControl the binding of ItemsSource is set to my ObservableCollection.在 ItemsControl 中的 XAML 中,ItemsSource 的绑定设置为我的 ObservableCollection。 In ItemsControl Datatemplate I bind ellipse properties to data stored in EllipseVM class and also add reference to my behavior class在 ItemsControl Datatemplate 中,我将椭圆属性绑定到存储在 EllipseVM class 中的数据,并且还添加了对我的行为的引用 class
  • in ItemsControl ItemContainerStyle canvas top and left properties are bound to my ObservableCollection在 ItemsControl ItemContainerStyle canvas 顶部和左侧属性绑定到我的 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.单击椭圆时,我的行为 class 访问数据上下文,找到 EllipseVM class 的实例,并根据鼠标 cursor position 相对于 canvas 更改 X 和 Y 坐标。

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:

  • View看法
  • VModel V模型
  • Model 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.视图的属性应该绑定到 VModel,您尝试正确地将视图与 EllipseVM 绑定,就像真正的专家一样,您项目的问题是您没有将单个视图绑定到单个 VM。 but you want an infinite number of VModels.但你想要无限数量的 VModel。

I will enumerate below some points of reflection:我将在下面列举一些反思点:

  • I would like to challenge the fact that you register on different drag events: element.MouseLeftButtonDown我想质疑您注册不同拖动事件的事实: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)? 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.TopCanvas.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 .现在,您的代码仅适用于EllipseEllipseVM

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中设置绑定,如下例所示(项目容器StyleDataContext始终是数据项,在您的情况下为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 .您甚至可以拖动ButtonDataTemplate中定义的任何内容。 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);
  }
}

Usage example使用示例

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM