繁体   English   中英

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

[英]C# WPF MVVM, attached behavior to update mainwindow datacontext

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

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

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

简短的应用程序描述:

  • 在应用程序启动时,TestWindow2VM class 的实例被创建并分配给 main window 作为数据上下文
  • TestWindow2VM class 包含 ObservableCollection,其中包含 EllipseVM class。
  • EllipseVM class 包含 X、Y 坐标和一些其他数据(画笔等)。
  • 在 ItemsControl 中的 XAML 中,ItemsSource 的绑定设置为我的 ObservableCollection。 在 ItemsControl Datatemplate 中,我将椭圆属性绑定到存储在 EllipseVM class 中的数据,并且还添加了对我的行为的引用 class
  • 在 ItemsControl ItemContainerStyle canvas 顶部和左侧属性绑定到我的 ObservableCollection
  • 单击椭圆时,我的行为 class 访问数据上下文,找到 EllipseVM class 的实例,并根据鼠标 cursor position 相对于 canvas 更改 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 不同的 3 种 object:

  • 看法
  • V模型
  • Model

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

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

  • 我想质疑您注册不同拖动事件的事实:element.MouseLeftButtonDown

您应该仅在创建或销毁对象时注册。

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

  • 避免通过像“CanBeDragged”这样的字符串注册属性找到一种方法来定义和使用接口,例如 IMoveFeature{bool IsDraggable}

您的代码太复杂并且有一些错误。
例如,不需要 CanvasDragBehavior 的CanvasDragBehavior实例属性。 看起来你在这里混淆了一些东西。

对于 position Canvas上的元素,只需使用附加属性Canvas.TopCanvas.Left

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

另一个重要的修复是使用WeakEventManager订阅附加元素的事件。 否则,您会造成潜在的 memory 泄漏(取决于事件发布者和事件侦听器的生命周期)。 始终记住遵循以下模式以避免此类 memory 泄漏:当您订阅事件时,请确保您也将始终取消订阅。 如果您无法控制 object 生命周期,请始终遵循弱事件模式并使用 Wea WeakEventManager来观察事件。
在您的情况下:当从ItemsControl.ItemsSource中删除一个项目时,您的行为将无法检测到此更改以取消订阅相应的事件。
在您的上下文中发生 memory 泄漏的风险并不高,但安全总比后悔好,并坚持安全模式。

在实现控件或行为时,尽量避免与数据类型和实现细节紧密耦合。 使控件或行为尽可能通用。 因此,您的行为不应了解DataContext以及拖动的元素类型。 这样您就可以简单地扩展您的代码或重用该行为,例如也允许拖动一个Rectangle 现在,您的代码仅适用于EllipseEllipseVM

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

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

使用示例

数据项.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>

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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