行为模式——WPF和Avalonia中的视觉行为与实际示例

目录

行为导论

什么是行为模式?

行为和MVVM

先决条件和其他方法

关于本文内容

例子

代码位置

可视元素上发生路由事件时调用方法的WPF行为。

可视元素上发生路由事件时调用方法的Avalonia行为

在一个视觉元素上使用两个行为实例

拖动行为示例

结论


行为导论

什么是行为模式?

有一种非常有趣的模式称为Behavior,主要用于可视化编程(WPFAvalonia),尽管它也可以很好地用于完全非可视化的代码。

从历史上看,AFAIK行为一词是由MS Blend SDK引入的,他们使用行为来实现自定义对象,一旦附加到视觉类,就会触发视觉类行为的变化。

一般来说,行为是附加到对象的东西,以非侵入性方式修改或增强对象的行为——无需修改对象的类本身:

C#行为通常是通过对对象的事件做出反应来实现的(在WPFAvalonia中,它们也可以对DependencyAttached属性更改做出反应)。一些更简单的行为只会在它们附加到对象或从对象分离的那一刻改变对象的状态——这样的行为甚至不需要对象有任何事件。

WPFAvalonia中,视觉行为最常用于在XAML视觉或逻辑树中某处发生事件时产生一些视觉变化。

行为和MVVM

当使用MVVM(模型-视图-视图模型)模式时,行为特别有用,因为正确使用该模式意味着通过绑定到某些非可视视图模型(没有任何代码隐藏)的DataTemplate定义视图。一方面,重要的是不要有代码隐藏,因为它经常用于恶意混合视觉和非视觉问题,并将XAML表示与C#代码紧密结合。另一方面,您可能仍然需要使用C#对涉及两个或多个属性的可视对象进行一些复杂的修改。在没有代码隐藏的情况下实现它的最佳方法是通过行为。

请注意,可视对象之间的某些通信可能通过视图模型本身完成,例如,ToggleButton's IsChecked属性可以双向绑定到视图模型上定义的属性,其更改将触发一些其他更改,这些更改将由其他可视化反映特性。这很好并且完全合法,但是在某些情况下,通过视图模型进行这种通信是不可取的或不可能的。例如

  1. 如果更改是纯视觉的和本地的(不影响更改发生的小逻辑区域之外的任何内容,特别是如果它不涉及任何业务逻辑),最好不要用不必要的额外代码污染您的视图模型。
  2. 如果我们需要基于一些路由事件(不是Button.ClickMenuItem.Click事件——我们可以使用命令)来改变一些视觉效果,那么我们就不得不使用背后的行为或代码。

先决条件和其他方法

为了充分利用本文,您需要了解一些基本的WPF/Avalonia概念,包括XAML、视觉和逻辑树、路由事件传播、附加属性和绑定。

对于那些刚接触Avalonia的人来说——它是一个非常相似但比WPF更强大的开源UI开发框架,而且非常重要的是——它也是多平台的——使用Avalonia构建的UI桌面应用程序可以在WindowsLinuxMac计算机上运行。Avalonia也即将发布一个通过WASM技术在浏览器中运行的版本。

如果您是AvaloniaWPF的初学者,您可以从以下文章开始:

  1. 在Easy Samples中使用AvaloniaUI进行多平台UI编码——第1部分——AvaloniaUI构建块
  2. 多平台Avalonia .NET Framework简单示例中的XAML基础知识
  3. 简单示例中的多平台Avalonia .NET Framework编程基本概念
  4. 简单示例中的多平台Avalonia .NET Framework编程高级概念

当涉及到行为时,有趣的问题是如何将行为对象附加到它修改或增强的对象上。从下面的示例中可以看出,一些行为可以实现为静态类,并在该行为中的某个附加属性在对象上获得某个值时附加到视觉对象。这是我创建行为的首选方式。

其他行为,例如Avalonia行为的行为是非静态对象,它们使用一些特殊的附加属性来附加行为。这些行为是AvaloniaUWP行为的重新实现,而这些行为又受到原始MS Blend行为的启发。

为了了解如何安装Avalonia Visual Studio扩展以及如何创建Avalonia项目,请查看使用Visual Studio 2019创建和运行简单的Avalonia项目

关于本文内容

不幸的是,行为不是非常简单的软件对象,从我的角度来看,为了理解行为,您需要了解它们是如何工作的。因此,本文描述了创建几个简单的自定义行为。

作为后续,我计划另一篇文章解释NP.Avalonia.Visuals包中的一些非常有用的行为。

您必须阅读文章并运行示例。如果您尝试创建具有自定义行为的类似示例项目,那就更好了。

我将从WPF行为的一个小例子开始,然后将重点介绍Avalonia,它是WPF的更好、更大和多平台的版本。

例子

代码位置

所有代码(包括单个WPF示例)都位于NP.Avalonia.Demos/CustomBehaviors下

我使用.NET 5.0VS2019来编写此代码,即使应该能够轻松地移植上或下。

可视元素上发生路由事件时调用方法的WPF行为。

该示例的代码可以在NP.Demos.WPFCallActionBehaviorSample下找到。

Visual Studio中打开、编译并运行解决方案。这是示例的主窗口:

如果将鼠标移到左侧的黄色方块上,窗口的背景将变为红色。如果将鼠标移到右侧的粉红色方块上,会弹出一个小对话框窗口,其中包含文本我是一个快乐的对话框!

现在,让我们看看这个功能的实现。

将窗口背景切换为红色并打开弹出窗口的方法在MainWindow.xaml.cs文件中定义,它们是void MakeWindowBackgroundRed()void OpenDialog()方法:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    // Turns window background red
    public void MakeWindowBackgroundRed()
    {
        Background = new SolidColorBrush(Colors.Red);
    }

    // opens a dialog
    public void OpenDialog()
    {
        Window dialogWindow =
            new Window()
            {
                Left = this.Left + 50,
                Top = this.Top + 50,
                Owner = this, 
                Width = 200, 
                Height = 200 
            };

        dialogWindow.Content = new TextBlock
        {
            HorizontalAlignment = HorizontalAlignment.Center,
            VerticalAlignment = VerticalAlignment.Center,
            Text = "I am a happy dialog!"
        };

        dialogWindow.ShowDialog();
    }
}  

MakeWindowBackgroundRed()方法在黄色方块上发生MouseEnter路由事件时被调用,在粉红色方块上发生相同事件时调用OpenDialog()

现在看一下XAML文件——MainWindow.xaml

<Window ...
        xmlns:local="clr-namespace:NP.Demos.WPFCallActionBehaviorSample"
        ...
        Width="400"
        Height="300">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Border Background="Yellow"
                HorizontalAlignment="Center"
                VerticalAlignment="Center" 
                local:CallActionOnEventBehavior.TheEvent="{x:Static UIElement.MouseEnterEvent}"
                local:CallActionOnEventBehavior.TargetObject="{Binding RelativeSource={RelativeSource AncestorType=Window}}"
                local:CallActionOnEventBehavior.MethodToCall="MakeWindowBackgroundRed"
                Width="50"
                Height="50"/>

        <Border Background="Pink"
                Grid.Column="1"
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                local:CallActionOnEventBehavior.TheEvent="{x:Static UIElement.MouseEnterEvent}"
                local:CallActionOnEventBehavior.TargetObject="{Binding RelativeSource={RelativeSource AncestorType=Window}}"
                local:CallActionOnEventBehavior.MethodToCall="OpenDialog"
                Width="50"
                Height="50" />
    </Grid>
</Window>  

请注意,我们将xmlns:local XML命名空间定义为指向我们项目的命名空间——NP.Demos.WPFCallActionBehaviorSample

两个正方形——黄色和粉红色被定义为两个50x50border。以下是我们如何在黄色方块上发生路由事件MouseEnter以调用MainWindow对象上的MakeWindowBackgroundRed()方法:

local:CallActionOnEventBehavior.TheEvent="{x:Static UIElement.MouseEnterEvent}"
local:CallActionOnEventBehavior.TargetObject="{Binding RelativeSource={RelativeSource AncestorType=local:MainWindow}}"
local:CallActionOnEventBehavior.MethodToCall="MakeWindowBackgroundRed"  

以上3行中设置的所有3个属性都是在项目本地的CallActionOnEventBehavior静态类中定义的静态附加属性。这个类将在下面简要说明。

现在让我们看一下上面这3行中设置的属性:

  1. local:CallActionOnEventBehavior.TheEvent="{x:Static UIElement.MouseEnterEvent}"将我们border对象上的行为的附加属性设置为UIElement类中定义的静态UIElement.MouseEnterEvent路由事件MouseEnterEvent(路由事件,就像附加属性一样,有一个静态字段来定义它们)
  2. local:CallActionOnEventBehavior.TargetObject="{Binding RelativeSource={RelativeSource AncestorType=local:MainWindow}}"——我们将border对象上的附加CallActionOnEventBehavior.TargetObject属性绑定到MainWindow视觉树的上方。请注意,由于我们使用附加属性,我们可以将它们作为绑定的目标。
  3. local:CallActionOnEventBehavior.MethodToCall="MakeWindowBackgroundRed"最后我们将方法名称设置为“MakeWindowBackgroundRed”方法。

粉红色方块以类似的方式定义其行为,只是方法名称是“OpenDialog”

让我们将注意力转移到实现该行为的静态类CallActionOnEventBehavior上。它定义了3个附加属性(我们在XAML文件中设置的相同属性):

  1. 类型 RoutedEventTheEvent——指定视觉对象应为其调用方法的路由事件。
  2. 类型objectTargetObject ——指定调用方法的对象。
  3. 类型stringMethodToCall——指定要调用的方法的名称。

TheEvent附加属性定义了属性更改时要触发的OnEventChanged回调:

public static readonly DependencyProperty TheEventProperty =
   DependencyProperty.RegisterAttached
   (
       "TheEvent",
       typeof(RoutedEvent),
       typeof(CallActionOnEventBehavior),
       new PropertyMetadata(default(RoutedEvent), OnEventChanged /* callback */)
   );

在该回调中,我们将视觉对象上的新事件连接到处理程序HandleRoutedEvent(...)(如果旧路由事件与同一处理程序不为空,也断开旧路由事件):

private static void OnEventChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    // we can only set the behavior on FrameworkElement - almost any visual element
    FrameworkElement el = (FrameworkElement)d;

    RoutedEvent oldRoutedEvent = e.OldValue as RoutedEvent;

    if (oldRoutedEvent != null)
    {
        // remove old event handler from the object (if exists)
        el.RemoveHandler(oldRoutedEvent, (RoutedEventHandler)HandleRoutedEvent);
    }

    RoutedEvent newRoutedEvent = e.NewValue as RoutedEvent;

    if (newRoutedEvent != null)
    {
        // add new event handler to the object
        el.AddHandler(newRoutedEvent, (RoutedEventHandler) HandleRoutedEvent);
    }
}
#endregion TheEvent attached Property  

void HandleRoutedEvent(...)方法的实现获取TargetObjectMethodToCall值并使用反射调用TargetObject上的MethodToCall方法:

// handle the routed event when happens on the object
// by calling the method of name 'methodName' onf the
// TargetObject
private static void HandleRoutedEvent(object sender, RoutedEventArgs e)
{
    FrameworkElement el = (FrameworkElement)sender;

    // if TargetObject is not set, use DataContext as the target object
    object targetObject = GetTargetObject(el) ?? el.DataContext;

    string methodName = GetMethodToCall(el);

    // do not do anything
    if (targetObject == null || methodName == null)
    {
        return;
    }

    MethodInfo methodInfo = 
        targetObject.GetType().GetMethod(methodName);

    if (methodInfo == null)
    {
        return;
    }

    // call the method using reflection
    methodInfo.Invoke(targetObject, null);
}  

当然,这个简单的行为中还有很多东西没有实现,例如假设只有一个TargetObject上的名为“MethodToCall”的方法,并且该方法没有参数。NP.Avalonia.Visuals开源项目的实际CallAction行为得到了更好的总结和更强大的功能。然而,我们CallActionOnEventBehavior对于理解静态行为是如何工作的已经足够了。

可视元素上发生路由事件时调用方法的Avalonia行为

NP.Demos.CallActionBehaviorSample包含一个与上面讨论的非常相似的项目,它使用Avalonia而不是WPF

运行项目,示例应用程序应该完全一样。

WPF项目的差异(除了一些Avalonia类型与相应的WPF类型命名不同)非常小。

主要区别在于我们如何设置TheEvent附加属性更改的回调。在WPF中,我们将回调作为参数之一传递给传递给附加属性定义的元数据:new PropertyMetadata(default(RoutedEvent), OnEventChanged /* callback */)

Avalonia中,我们使用Reactive Extensions(Rx)来订阅CallActionOnEventBehavior类的静态构造函数中属性的更改:

public class CallActionOnEventBehavior
{
   ...
   static CallActionOnEventBehavior()
   {
      TheEventProperty.Changed.Subscribe(OnEventChanged);
   }
   ...
}

在一个视觉元素上使用两个行为实例

静态行为的一个问题是您不能在同一个视觉元素上使用它们的多个实例。例如使用CallActionOnEventBehavior我们可以在PointerEnter事件上调用MakeWindowBackgroundRed()方法,但我们不能在不同的事件上调用不同的方法,例如在PointerLeave事件上的RestoreBackground()方法。

一般来说,根据我的经验,将相同类型的行为附加到相同元素的这种要求非常罕见,当需要时,有一个技巧可以实现这一点。

NP.Demos.DoubleCallActionBehaviorSample行为给出了在同一元素上模拟两次调用CallActionOnEventBehavior的示例。

与上一个示例相比的更改仅在MainWindow.axaml.csMainWindow.axaml文件中。

MainWindow类现在有一个额外的RestoreBackground()方法,可以将Window的背景恢复到变为红色之前的状态:

private IBrush? _oldBackground = null;
// Turns window background red
public void MakeWindowBackgroundRed()
{
    _oldBackground = Background;
    Background = new SolidColorBrush(Colors.Red);
}

public void RestoreBackground()
{
    Background = _oldBackground;
}  

MainWindow.asaml文件中,将窗口变为红色的黄色Border鼠标现在成为透明Grid面板的子级。该Grid面板仅用于调用第二个PointerLeave事件的行为(从Border冒泡到Grid):

<Grid local:CallActionOnEventBehavior.TheEvent="{x:Static InputElement.PointerLeaveEvent}"
      local:CallActionOnEventBehavior.TargetObject="{Binding RelativeSource={RelativeSource AncestorType=local:MainWindow}}"
      local:CallActionOnEventBehavior.MethodToCall="RestoreBackground"					
      HorizontalAlignment="Center"
      VerticalAlignment="Center"
      Width="50"
      Height="50">
    <Border Background="Yellow"
            local:CallActionOnEventBehavior.TheEvent="{x:Static InputElement.PointerEnterEvent}"
            local:CallActionOnEventBehavior.TargetObject="{Binding RelativeSource={RelativeSource AncestorType=local:MainWindow}}"
            local:CallActionOnEventBehavior.MethodToCall="MakeWindowBackgroundRed"				
            HorizontalAlignment="Stretch"
            VerticalAlignment="Stretch"/>
</Grid>  

生成的示例行为正是我们想要的——当鼠标进入黄色方块时Window变为红色,然后在离开时变回白色。

拖动行为示例

前三个示例都是围绕处理一些路由事件和调用方法的相同行为构建的。在这个示例中,我们将演示一个更复杂的行为,它允许在窗口内拖动控件。

Drag示例代码位于NP.Demos.DragBehaviorSample项目下。

打开、编译并运行该项目——您将在窗口的左右垂直部分的中间看到一个粉红色的圆圈和一个蓝色的方块。您可以通过在它们上按下鼠标左键并将它们拖动到窗口中的任何位置来移动它们:

重要的代码位于DragBehavior.csMainWindow.axaml文件中。

MainWindow.axaml非常简单——窗口包含一个带有两列的网格。左列有一个粉色椭圆,右列有一个蓝色矩形:

<Window ...
        xmlns:local="clr-namespace:NP.Demos.DragBehaviorSample"
        ...>
    <Grid ColumnDefinitions="*, *">
        <Ellipse Width="30"
                 Height="30"
                 Fill="Pink"
                 HorizontalAlignment="Center"
                 VerticalAlignment="Center"
                 local:DragBehavior.IsSet="True"/>

        <Rectangle Width="30"
                   Height="30"
                   Fill="Blue"
                   Grid.Column="1"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   local:DragBehavior.IsSet="True"/>
    </Grid>
</Window>  

文件中最有趣的行是通过设置附加属性local:DragBehavior.IsSettrue来设置椭圆和矩形行为的行。

查看DragBehavior.cs文件。它包含三个附加属性:

  1. bool IsSet——一旦在控件上设置为true -控件将变为可拖动的。
  2. Point InitialPointerLocation——设置为拖动操作开始时窗口内的指针位置。
  3. Point InitialDragShift——设置为拖动操作开始时控件的位移(相对于原始位置)。

IsSet附加属性的回调将处理程序设置为PointerPressed事件,并在该属性设置为true时将RenderTransform赋值为TranslateTransform(这将移动控件)。如果该属性设置为false,则会发生相反的情况——PointerPressed处理程序被删除并且RenderTransform变为null

static DragBehavior()
{
    IsSetProperty.Changed.Subscribe(OnIsSetChanged);
}

// set the PointerPressed handler when 
private static void OnIsSetChanged(AvaloniaPropertyChangedEventArgs<bool> args)
{
    IControl control = (IControl) args.Sender;

    if (args.NewValue.Value == true)
    {
        // connect the pointer pressed event handler
        control.RenderTransform = new TranslateTransform();
        control.PointerPressed += Control_PointerPressed;
    }
    else
    {
        // disconnect the pointer pressed event handler
        control.RenderTransform = null;
        control.PointerPressed -= Control_PointerPressed;
    }
}  

看一下Control_PointerPressed(...)处理程序——当Drag操作开始时它会触发:

// start drag by pressing the point on draggable control
private static void Control_PointerPressed(object? sender, PointerPressedEventArgs e)
{
    IControl control = (IControl)sender!;

    // capture the pointer on the control
    // meaning - the mouse pointer will be producing the
    // pointer events on the control
    // even if it is not directly above the control
    e.Pointer.Capture(control);

    // calculate the drag-initial pointer position within the window
    Point currentPointerPositionInWindow = GetCurrentPointerPositionInWindow(control, e);

    // record the drag-initial pointer position within the window
    SetInitialPointerLocation(control, currentPointerPositionInWindow);

    Point startControlPosition = GetShift(control);

    // record the drag-initial shift of the control
    SetInitialDragShift(control, startControlPosition);

    // add handler to do the shift and 
    // other processing on PointerMoved
    // and PointerReleased events. 
    control.PointerMoved += Control_PointerMoved;
    control.PointerReleased += Control_PointerReleased;
}  

我们在控件中捕获鼠标,获取指针位置的初始值和控件的初始移位,并相应地在InitialPointerLocationInitialDragShift附加属性中记录这些值。我们还为控件设置了处理程序PointerMovedPointerReleased事件——这些处理程序将在拖动操作结束时释放:

以下是我们在PointerMoved事件中所做的:

// update the shift when pointer is moved
private static void Control_PointerMoved(object? sender, PointerEventArgs e)
{
    IControl control = (IControl)sender!;
    // Shift control to the current position
    ShiftControl(control, e);
} 

本质上我们只调用ShiftControl方法:

// modifies the shift on the control during the drag
// this essentially moves the control
private static void ShiftControl(IControl control, PointerEventArgs e)
{
    // get the current pointer location
    Point currentPointerPosition = GetCurrentPointerPositionInWindow(control, e);

    // get the pointer location when Drag operation was started
    Point startPointerPosition = GetInitialPointerLocation(control);

    // diff is how far the pointer shifted
    Point diff = currentPointerPosition - startPointerPosition;

    // get the original shift when the drag operation started
    Point startControlPosition = GetInitialDragShift(control);

    // get the resulting shift as the sum of 
    // pointer shift during the drag and the original shift
    Point shift = diff + startControlPosition;

    // set the shift on the control
    SetShift(control, shift);
}  

该方法获取指针的当前位置,获取它与拖动操作开始时指针位置的差值,并将该差值与拖动操作开始时的控件移位相加,得到所需的当前移位控制。

Control_PointerReleased处理程序释放捕获,并在将控件移动到拖动操作的最终位置的顶部删除PointerMovedPointerReleased处理程序:

// Drag operation ends when the pointer is released. 
private static void Control_PointerReleased(object? sender, PointerReleasedEventArgs e)
{
    IControl control = (IControl)sender!;

    // release the capture
    e.Pointer.Capture(null);

    ShiftControl(control, e);

    // disconnect the handlers 
    control.PointerMoved -= Control_PointerMoved;
    control.PointerReleased -= Control_PointerReleased;
}  

结论

本文描述了非常有用的行为模式——允许以非侵入方式修改对象行为的功能——无需修改对象类的代码。行为可以使用可观察对象(包括事件)来检测更改并根据这些更改修改对象的属性。

https://www.codeproject.com/Articles/5320737/Behavior-Pattern-Visual-Behaviors-in-WPF-and-Avalo

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值