0

I am trying to create a generic status indicator display for a WPF HMI application. These status indicators are a user control wherein two concentric circles of different radius overlap. I want to be able to change the colour of the "fill" property on the path tag depending on some dependency properties of my StatusIndicator class. In practice, there are an arbitrary number of these indicators that may be used. The 'state' of these indicators is handled by a class object, DigitalIOAssignment, which gets its data (componentID, isActive, isInterlocked, etc.) from a PLC concerning the state of a given I/O component. Since the number of these status indicators is arbitrary, I create a List <DigitalIOAssignment> and pass this to my viewmodel. This is working correctly and I can see the data I want to bind correctly in my viewmodel.

The status indicator is coded as follows:

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>

Code Behind:

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

In my view's xaml, I create each status indicator in the designer and hard-code a x:Name to it and assign this to StatusIndicatorName since I can't figure out how to pass this Name value at runtime to the code-behind (any hints would be appreciated!!). What I want to do is this:

  1. Create a StatusIndicator user control and assign the StatusIndicatorName property a known string
  2. UserControls:StatusIndicator.ComponentID property is bound to DigitalIOAssignment.componentID
  3. It is my hope that binding to the List causes an iteration over this list and to engage a <DataTrigger> that will allow me to reference the same DigitalIOAssignment object when the trigger condition is met, and set the appropriate flags (isActive, isInterlocked etc) in this way. This pseudocode represents, I hope, what I am trying to do in my view's 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>

Obviously, this implementation does not work. I cannot use a binding for a value on a data trigger (I may have to hard-code the component ID I am expecting since I hard-code the status indicator name anyway), and I cannot seem to use setters for my dependency properties. I get an error:

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

Can someone please give me some insight how to approach this problem for what I am trying to achieve? Even if I need to start over and approach it a different way? Thank you!

jamesnet214
  • 1,044
  • 13
  • 21
mooreppj
  • 23
  • 4

1 Answers1

1

I'm not 100% sure I follow what you're after. You have an arbitrary number of DigitalIOAssignment's which are held in a List in the VM, and you want to create a StatusIndicator in the view for each of them?

The usual way to do this is use an ItemsControl in the view, with a DataTemplate that has a single StatusIndicator. If you bind ItemsControl.ItemsSource to your list, wpf will apply the template for every item in the list, and the DataContext of the template will be that item, so you can do straight bindings with no need for triggers.

Something like:

<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>
GazTheDestroyer
  • 20,722
  • 9
  • 70
  • 103
  • Thanks for the response Gaz! Your assessment is correct: I have an arbitrary number of DigitalIOAssignments (number is based on some arrays in a PLC) and have an arbitrary (but not necessarily equal) number of status indicators depending on what is required to display; I would add these to the xaml manually for a given project and want to give them identifying names in the xaml. When I try this method with an ItemsControl, it is not clear how I am supposed to differentiate specific StatusIndicators. The DigitalIOAssignment has no knowledge of the view or how many indicators I am showing. – mooreppj May 14 '21 at 14:46
  • 1
    If you require specific indicators on the view, then just expose specific DigitalIOAssignments as properties on your VM and bind them individually. That's the whole point of a VM, to manipulate model data into a data structure that represents the view. – GazTheDestroyer May 14 '21 at 14:50