在 C# WPF 中使用依赖属性时,如何正确利用数据触发器和设置器?

Posted

技术标签:

【中文标题】在 C# WPF 中使用依赖属性时,如何正确利用数据触发器和设置器?【英文标题】:How do I properly utilize datatriggers and setters when using dependency properties in C# WPF? 【发布时间】:2021-08-04 15:17:26 【问题描述】:

我正在尝试为 WPF HMI 应用程序创建通用状态指示器显示。这些状态指示器是用户控件,其中两个不同半径的同心圆重叠。我希望能够根据我的StatusIndicator 类的一些依赖属性来更改路径标记上“填充”属性的颜色。在实践中,可以使用任意数量的这些指标。这些指示器的“状态”由类对象DigitalIOAssignment 处理,该对象从PLC 获取有关给定I/O 组件状态的数据(componentID、isActive、isInterlocked 等)。由于这些状态指示器的数量是任意的,因此我创建了一个 List <DigitalIOAssignment> 并将其传递给我的视图模型。这工作正常,我可以在我的视图模型中看到我想要正确绑定的数据。

状态指示器编码如下:

XAML:

<UserControl x:Class="HMI.UserControls.StatusIndicator"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:prism="http://prismlibrary.com/"              
            xmlns:local="clr-namespace:HMI.UserControls" 
            xmlns:globals="clr-namespace:HMI.LogixPLCService.Globals;assembly=HMI.LogixPLCService" 
            mc:Ignorable="d" 
            d:DesignHeight="100" d:DesignWidth="100">
    <Viewbox x:Name="ControlViewbox" Stretch="Uniform" Height="auto" Width="auto">
        <Canvas x:Name="ControlCanvas" Width="100" Height="100">
            <!-- Draw Secondary Indicator Body First -->
            <Path x:Name="StatusIndicator_Body" Width="100" Height="100" 
                  Canvas.Left="0" Canvas.Top="0" StrokeThickness="1"
                  StrokeMiterLimit="2.75" Stroke="Black">
                <Path.Data>
                    <EllipseGeometry Center="50,50" RadiusX="50" RadiusY="50"/>
                </Path.Data>
                <Path.Style>
                    <Style TargetType="Path">
                        <Setter Property="Fill" Value="LightGray"/>
                        <Style.Triggers>
                            <DataTrigger Binding="Binding RelativeSource=RelativeSource AncestorType=x:Type local:StatusIndicator, Path=isInterlockedProperty"
                                         Value="True">
                                <Setter Property="Fill" Value="Yellow"/>
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </Path.Style>
            </Path>
            <!-- Draw Foreground Indicator Body Second -->
            <Path x:Name="StatusIndicator_Status" Width="100" Height="100" 
                  Canvas.Left="0" Canvas.Top="0" StrokeThickness=".5"
                  StrokeMiterLimit="1" Stroke="Black">
                <Path.Data>
                    <EllipseGeometry Center="50,50" RadiusX="30" RadiusY="30"/>
                </Path.Data>
                <Path.Style>
                    <Style TargetType="Path">
                        <Setter Property="Fill" Value="DarkGray"/>
                        <Style.Triggers>
                            <DataTrigger Binding="Binding RelativeSource=RelativeSource AncestorType=x:Type local:StatusIndicator, Path=isActiveProperty" 
                                         Value="True">
                                <Setter Property="Fill" Value="Lime"/>
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </Path.Style>
            </Path>
        </Canvas>
    </Viewbox>
</UserControl>

代码背后:

namespace HMI.UserControls

    public partial class StatusIndicator : UserControl
    
        /// <summary>
        /// Interaction logic for StatusIndicator.xaml
        ///</summary>
        public string StatusIndicatorName
        
            get  return (string)GetValue(StatusIndicatorNameProperty); 
            set  SetValue(StatusIndicatorNameProperty, value); 
        
        public static readonly DependencyProperty StatusIndicatorNameProperty = 
            DependencyProperty.Register("StatusIndicatorName", 
                typeof(string), typeof(StatusIndicator), new PropertyMetadata(null));

        public string ComponentID
        
            get  return (string)GetValue(ComponentIDProperty); 
            set  SetValue(ComponentIDProperty, value); 
        
    
        public static readonly DependencyProperty ComponentIDProperty = 
            DependencyProperty.Register("ComponentID", 
                typeof(string), typeof(StatusIndicator), new PropertyMetadata(null));

        public bool isActiveProperty
        
            get  return (bool)GetValue(isActive); 
            set  SetValue(isActive, value); 
        
        public static readonly DependencyProperty isActive = 
            DependencyProperty.Register("isActiveProperty", 
                typeof(bool), typeof(StatusIndicator), new PropertyMetadata(false));

        public bool isInterlockedProperty
        
            get  return (bool)GetValue(isInterlocked); 
            set  SetValue(isInterlocked, value); 
        

        public static readonly DependencyProperty isInterlocked = 
            DependencyProperty.Register("isInterlockedProperty", 
                typeof(bool), typeof(StatusIndicator), new PropertyMetadata(false));

        public StatusIndicator()
        
            InitializeComponent();
        
    

在我的视图的 xaml 中,我在设计器中创建每个状态指示器,并将 x:Name 硬编码给它,并将其分配给 StatusIndicatorName,因为我不知道如何在运行时将此 Name 值传递给代码隐藏(任何提示将不胜感激!!)。我想做的是这样的:

    创建一个 StatusIndicator 用户控件并为 StatusIndicatorName 属性分配一个已知字符串 UserControls:StatusIndicator.ComponentID 属性绑定到 DigitalIOAssignment.componentID 我希望绑定到 List 会导致对该列表进行迭代并使用 &lt;DataTrigger&gt;,这将允许我在满足触发条件时引用相同的 DigitalIOAssignment 对象,并设置适当的标志 ( isActive、isInterlocked 等)以这种方式。我希望这个伪代码代表了我在视图的 Xaml 中尝试做的事情:
<UserControls:StatusIndicator x:Name="DI_99VLV01"
                              StatusIndicatorName="Binding ElementName=DI_99VLV01"                                      
                              Height="18" Width="18"
                              Margin="106,144,0,0"
                              HorizontalAlignment="Left" VerticalAlignment="Top"       
                              ComponentID="Binding privateDigitalInputAssignments/componentID">
    <DataTrigger Binding="Binding Path=(UserControls:StatusIndicator.ComponentID)"
                 Value="Binding Path=(UserControls:StatusIndicator.StatusIndicatorName)">
        <Setter Property="UserControls:StatusIndicator.isActiveProperty"
                Value="Binding privateDigitalInputAssignments/isActive"/>
        <Setter Property="UserControls:StatusIndicator.isInterlockedProperty" 
                Value="Binding privateDigitalInputAssignments/isInterlocked"/>
    </DataTrigger>
</UserControls:StatusIndicator>

显然,此实现不起作用。我不能对数据触发器上的值使用绑定(我可能必须对我期望的组件 ID 进行硬编码,因为无论如何我都对状态指示器名称进行了硬编码),而且我似乎无法对我的依赖项属性使用 setter。我收到一个错误:

Cannot find the static member 'isActivePropertyProperty' [sic!] on the type 'StatusIndicator'.

有人可以给我一些见解如何解决这个问题,以实现我想要实现的目标吗?即使我需要重新开始并以不同的方式处理它?谢谢!

【问题讨论】:

【参考方案1】:

我不能 100% 确定我会遵循您的要求。您有任意数量的DigitalIOAssignment,它们保存在 VM 的列表中,并且您想在视图中为每个StatusIndicator 创建一个StatusIndicator

通常的方法是在视图中使用ItemsControl,而DataTemplate 有一个StatusIndicator。如果您将ItemsControl.ItemsSource 绑定到您的列表,wpf 将为列表中的每个项目应用模板,并且模板的DataContext 将是该项目,因此您可以直接绑定而无需触发器。

类似:

<ItemsControl ItemsSource="Binding DigitalInputAssignments">
  <ItemsControl.ItemTemplate>
    <DataTemplate>

      <UserControls:StatusIndicator Height="18" Width="18"
                                    Margin="106,144,0,0"
                                    HorizontalAlignment="Left" VerticalAlignment="Top"
                                    StatusIndicatorName="Binding Name"
                                    ComponentID="Binding ComponentID"
                                    IsActive="Binding IsActive"
                                    IsInterlocked="Binding IsInterlocked">
    </DataTemplate>
  </ItemsControl.ItemTemplate>
</ItemsControl>

【讨论】:

感谢盖兹的回复!您的评估是正确的:我有任意数量的 DigitalIOAssignments(数量基于 PLC 中的某些数组)并且有任意(但不一定相等)数量的状态指示器,具体取决于显示的内容;我会为给定项目手动将这些添加到 xaml 中,并希望在 xaml 中为它们提供标识名称。当我使用 ItemsControl 尝试此方法时,不清楚我应该如何区分特定的 StatusIndicators。 DigitalIOAssignment 不知道视图或我显示了多少指标。 如果您需要视图上的特定指标,那么只需将特定的 DigitalIOAssignments 公开为您的 VM 上的属性并单独绑定它们。这就是 VM 的全部意义所在,将模型数据操作到表示视图的数据结构中。

以上是关于在 C# WPF 中使用依赖属性时,如何正确利用数据触发器和设置器?的主要内容,如果未能解决你的问题,请参考以下文章

WPF快速入门系列——深入解析依赖属性

WPF中的依赖属性

WPF利用依赖属性和命令编写自定义控件

C#:Caliburn Micro:如何使用数据绑定控制 WPF 按钮的属性?

在 C# 中正确使用 WPF 绘制图表

WPF 依赖属性