0

I'm trying to implement a view with a DataGrid to display real time data from a controller, and have the possibility of enabling writing data from a control in the list back to the controller. The control should be dynamically selected depending on the data signal's datatype. In my current approach I using a button if the data type is a boolean, else the control in the cell should be a slider.

The problem is that I only get one visible instance each of the different controls.

I tried with both a DataGrid control and a ListView with GridView as you can see in the 2 tabs:

ListView tab.

DataGrid tab.

If I click twice on the GridView, the control gets visible in the selected row, and disappears in the other row. In the ListView, nothing happends if I click in any of the cells with no visible control.

I'm using the Material Design In XAML Toolkit in this example, but I get the same result without adding it.

I guess I can't use the CellTemplate's as I'm trying to in the view, but I hope there is a way to get it working.

Code:

MainWindow.xaml

<Window x:Class="ListContentControlTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ListContentControlTest"
        xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
        TextElement.Foreground="{DynamicResource MaterialDesignBody}"
        TextElement.FontWeight="Regular"
        TextElement.FontSize="13"
        TextOptions.TextFormattingMode="Ideal" 
        TextOptions.TextRenderingMode="Auto"
        Background="{DynamicResource MaterialDesignPaper}"
        FontFamily="{materialDesign:MaterialDesignFont}" 
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
   <Grid VerticalAlignment="Stretch" HorizontalAlignment='Stretch'>
      <TabControl>
         <TabItem Header="ListView">
            <ListView ItemsSource="{Binding DataSignalsList}">
               <ListView.View>
                  <GridView>
                     <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}"  Width="200" />
                     <GridViewColumn Header="Address" DisplayMemberBinding="{Binding Address}" Width="200" />
                     <GridViewColumn Header="Read" DisplayMemberBinding="{Binding Value}" Width="100" />
                     <GridViewColumn Header="Write">
                        <GridViewColumn.CellTemplate>
                           <DataTemplate>
                              <ContentControl>
                                 <ContentControl.Style>
                                    <Style TargetType="ContentControl">
                                       <Style.Triggers>
                                          <DataTrigger Binding="{Binding IsDataTypeBool}" Value="true">
                                             <Setter Property="Content">
                                                <Setter.Value>
                                                   <Button Content="Click Me" FontSize="10" Height="18"/>
                                                </Setter.Value>
                                             </Setter>
                                          </DataTrigger>
                                          <DataTrigger Binding="{Binding IsDataTypeBool}" Value="false">
                                             <Setter Property="Content">
                                                <Setter.Value>
                                                   <StackPanel Orientation="Horizontal">
                                                      <TextBlock Text="0" Margin="10 5 10 5"/>
                                                      <Slider Value="50" Minimum="0" Maximum="100" MinWidth="150"/>
                                                      <TextBlock Text="100" Margin="10 5 10 5"/>
                                                   </StackPanel>
                                                </Setter.Value>
                                             </Setter>
                                          </DataTrigger>
                                       </Style.Triggers>
                                    </Style>
                                 </ContentControl.Style>
                              </ContentControl>
                           </DataTemplate>
                        </GridViewColumn.CellTemplate>
                     </GridViewColumn>

                  </GridView>
               </ListView.View>
            </ListView>
         </TabItem>
         <TabItem Header="GridView">
            <DataGrid 
                    ItemsSource="{Binding DataSignalsList}"
                    CanUserSortColumns="True"
                    CanUserResizeRows="False"
                    CanUserAddRows="False"
                    AutoGenerateColumns="False"
                    HeadersVisibility="All" >
               <DataGrid.Columns>
                  <DataGridTextColumn Header="Name" Binding="{Binding Name}" />
                  <DataGridTextColumn Header="Address" Binding="{Binding Address}" />
                  <DataGridTextColumn CanUserSort="False" CanUserReorder="False" Header="Read" Binding="{Binding Value}"/>
                  <DataGridTemplateColumn Header="Write" MinWidth="250">
                     <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                           <ContentControl>
                              <ContentControl.Style>
                                 <Style TargetType="ContentControl">
                                    <Style.Triggers>
                                       <DataTrigger Binding="{Binding IsDataTypeBool}" Value="true">
                                          <Setter Property="Content">
                                             <Setter.Value>
                                                <StackPanel Orientation="Horizontal" Margin="5">
                                                   <Button Content="Click Me" FontSize="10" Height="18"/>
                                                </StackPanel>
                                             </Setter.Value>
                                          </Setter>
                                       </DataTrigger>
                                       <DataTrigger Binding="{Binding IsDataTypeBool}" Value="false">
                                          <Setter Property="Content">
                                             <Setter.Value>
                                                <StackPanel Orientation="Horizontal">
                                                   <TextBlock Text="0" Margin="10 5 10 5"/>
                                                   <Slider Value="50" Minimum="0" Maximum="100" MinWidth="150"/>
                                                   <TextBlock Text="100" Margin="10 5 10 5"/>
                                                </StackPanel>
                                             </Setter.Value>
                                          </Setter>
                                       </DataTrigger>
                                    </Style.Triggers>
                                 </Style>
                              </ContentControl.Style>
                           </ContentControl>
                        </DataTemplate>
                     </DataGridTemplateColumn.CellTemplate>
                  </DataGridTemplateColumn>
               </DataGrid.Columns>
            </DataGrid>
         </TabItem>
      </TabControl>
   </Grid>
</Window>

MainWindow.xaml.cs

using System.Windows;

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

MainWindowViewModel.cs

using ListContentControlTest.Models;
using System.Collections.Generic;

namespace ListContentControlTest.ViewModels
{
    public class MainWindowViewModel
    {
        private IList<DataSignalModel> _dataSignalsList;

        public MainWindowViewModel()
        {
            DataSignalsList = new List<DataSignalModel>
            {
                new DataSignalModel{Name = "Data Signal 0", Address = "Float Data Type Address", DataType = DataType.Float},
                new DataSignalModel{Name = "Data Signal 1", Address = "Bool Data Type Address", DataType = DataType.Bool},
                new DataSignalModel{Name = "Data Signal 2", Address = "Bool Data Type Address", DataType = DataType.Bool},
                new DataSignalModel{Name = "Data Signal 3", Address = "Bool Data Type Address", DataType = DataType.Bool},
                new DataSignalModel{Name = "Data Signal 4", Address = "Bool Data Type Address", DataType = DataType.Bool},
                new DataSignalModel{Name = "Data Signal 5", Address = "Float Data Type Address", DataType = DataType.Float},
                new DataSignalModel{Name = "Data Signal 6", Address = "Float Data Type Address", DataType = DataType.Float},
                new DataSignalModel{Name = "Data Signal 7", Address = "Bool Data Type Address", DataType = DataType.Bool},
                new DataSignalModel{Name = "Data Signal 8", Address = "Bool Data Type Address", DataType = DataType.Bool},
                new DataSignalModel{Name = "Data Signal 9", Address = "Bool Data Type Address", DataType = DataType.Bool}
            };
        }
        public IList<DataSignalModel> DataSignalsList
        {
            get { return _dataSignalsList; }
            set { _dataSignalsList = value; }
        }
    }
}

DataSignalModel.cs

using System.ComponentModel;

namespace ListContentControlTest.Models
{
    public enum DataType
    {
        Bool,
        Int,
        Float
    }

    public class DataSignalModel : INotifyPropertyChanged
    {
        bool _isDataTypeBool;
        private DataType _dataType;

        public string Name { get; set; }

        public DataType DataType
        {
            get => _dataType;
            set
            {
                _dataType = value;
                if (_dataType == DataType.Bool)
                {
                    IsDataTypeBool = true;
                }
                else
                {
                    IsDataTypeBool = false;
                }

                OnPropertyChanged(nameof(DataType));
                OnPropertyChanged(nameof(IsDataTypeBool));
            }
        }

        public string Address { get; set; }
      
        public double Value { get; set; }

        public bool IsDataTypeBool
        {
            get => _isDataTypeBool;
            set
            {
                _isDataTypeBool = value;
                OnPropertyChanged(nameof(IsDataTypeBool));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged(string propName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
        }
    }
}

App.xaml

<Application x:Class="ListContentControlTest.App"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:local="clr-namespace:ListContentControlTest">
<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Light.xaml" />
            <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml" />
            <ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Primary/MaterialDesignColor.LightBlue.xaml" />
            <ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Accent/MaterialDesignColor.Purple.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

App.xaml.cs

using System.Windows;
using ListContentControlTest.ViewModels;

namespace ListContentControlTest
{
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);
            MainWindow window = new MainWindow();
            MainWindowViewModel mwvm = new MainWindowViewModel();
            window.DataContext = mwvm;
            window.Show();
        }
    }
}
thatguy
  • 21,059
  • 6
  • 30
  • 40
NorwE
  • 79
  • 1
  • 4
  • Are you hitting the issue where DataTriggers don't read the initial value? Try googling wpf datatriggeronload. – Brannon Dec 12 '20 at 13:10

1 Answers1

2

The behavior that you see is caused by the way you create your data templates. The ContentControl inside is instantiated for each cell, but the controls inside the setters of the styles are not. That means a single instance of both the button and the slider are shared among all cells. As a control in WPF can only have a single parent - or in other words can only appear once in the visual tree - it is only reassigned or moved between the cells.

In order to resolve the issue, do not create controls in a style, but in a data template, e.g.:

<!-- Data template for "Bool" -->
<DataTemplate x:Key="DataSignalModelBoolTemplate" DataType="{x:Type local:DataSignalModel}">
   <StackPanel Orientation="Horizontal" Margin="5">
      <Button Content="Click Me" FontSize="10" Height="18"/>
   </StackPanel>
</DataTemplate>

<!-- Data template for both "Int" and "Float", hence "Numeric" -->
<DataTemplate x:Key="DataSignalModelNumericTemplate" DataType="{x:Type local:DataSignalModel}">
   <StackPanel Orientation="Horizontal">
      <TextBlock Text="0" Margin="10 5 10 5"/>
      <Slider Value="50" Minimum="0" Maximum="100" MinWidth="150"/>
      <TextBlock Text="100" Margin="10 5 10 5"/>
   </StackPanel>
</DataTemplate>

As you store a data type member instead of creating two distinct model types, you will have to create a custom DataTemplateSelector for selecting the data template depending on DataType.

public class DataSignalModelTemplateSelector : DataTemplateSelector
{
   public override DataTemplate SelectTemplate(object item, DependencyObject container)
   {
      if (!(item is DataSignalModel dataSignalModel) || !(container is FrameworkElement containerFrameworkElement))
         return null;

      switch (dataSignalModel.DataType)
      {
         case DataType.Bool:
            return FindDataTemplate(containerFrameworkElement, "DataSignalModelBoolTemplate");
         case DataType.Int:
         case DataType.Float:
            return FindDataTemplate(containerFrameworkElement, "DataSignalModelNumericTemplate");
         default:
            throw new ArgumentOutOfRangeException();
      }
   }

   private static DataTemplate FindDataTemplate(FrameworkElement frameworkElement, string key)
   {
      return (DataTemplate)frameworkElement.FindResource(key);
   }
}

Create an instance of this converter in a resource dictionary, where you store the data templates, too.

<local:DataSignalModelTemplateSelector x:Key="DataSignalModelTemplateSelector"/>

Then, adapt your column definitions in both the ListView and the DataGrid like this:

<GridViewColumn Header="Write" CellTemplateSelector="{StaticResource DataSignalModelTemplateSelector}"/>
<DataGridTemplateColumn Header="Write" MinWidth="250" CellTemplateSelector="{StaticResource DataSignalModelTemplateSelector}"/>

That's all. The data template selector will automatically determine the appropriate template and search in the resources for it. You can remove IsDataTypeBool from DataSignalModel, as it is not needed.

For convenience, here is the complete markup for your MainWindow:

<Window x:Class="ListContentControlTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ListContentControlTest"
        xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
        TextElement.Foreground="{DynamicResource MaterialDesignBody}"
        TextElement.FontWeight="Regular"
        TextElement.FontSize="13"
        TextOptions.TextFormattingMode="Ideal" 
        TextOptions.TextRenderingMode="Auto"
        Background="{DynamicResource MaterialDesignPaper}"
        FontFamily="{materialDesign:MaterialDesignFont}" 
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
   <Window.Resources>
      <local:DataSignalModelTemplateSelector x:Key="DataSignalModelTemplateSelector"/>
      <DataTemplate x:Key="DataSignalModelBoolTemplate" DataType="{x:Type local:DataSignalModel}">
         <StackPanel Orientation="Horizontal" Margin="5">
            <Button Content="Click Me" FontSize="10" Height="18"/>
         </StackPanel>
      </DataTemplate>
      <DataTemplate x:Key="DataSignalModelNumericTemplate" DataType="{x:Type local:DataSignalModel}">
         <StackPanel Orientation="Horizontal">
            <TextBlock Text="0" Margin="10 5 10 5"/>
            <Slider Value="50" Minimum="0" Maximum="100" MinWidth="150"/>
            <TextBlock Text="100" Margin="10 5 10 5"/>
         </StackPanel>
      </DataTemplate>
   </Window.Resources>
   <Grid VerticalAlignment="Stretch" HorizontalAlignment='Stretch'>
      <TabControl>
         <TabItem Header="ListView">
            <ListView ItemsSource="{Binding DataSignalsList}">
               <ListView.View>
                  <GridView>
                     <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}"  Width="200" />
                     <GridViewColumn Header="Address" DisplayMemberBinding="{Binding Address}" Width="200" />
                     <GridViewColumn Header="Read" DisplayMemberBinding="{Binding Value}" Width="100" />
                     <GridViewColumn Header="Write" CellTemplateSelector="{StaticResource DataSignalModelTemplateSelector}"/>
                  </GridView>
               </ListView.View>
            </ListView>
         </TabItem>
         <TabItem Header="GridView">
            <DataGrid 
                ItemsSource="{Binding DataSignalsList}"
                CanUserSortColumns="True"
                CanUserResizeRows="False"
                CanUserAddRows="False"
                AutoGenerateColumns="False"
                HeadersVisibility="All" >
               <DataGrid.Columns>
                  <DataGridTextColumn Header="Name" Binding="{Binding Name}" />
                  <DataGridTextColumn Header="Address" Binding="{Binding Address}" />
                  <DataGridTextColumn CanUserSort="False" CanUserReorder="False" Header="Read" Binding="{Binding Value}"/>
                  <DataGridTemplateColumn Header="Write" MinWidth="250" CellTemplateSelector="{StaticResource DataSignalModelTemplateSelector}"/>
               </DataGrid.Columns>
            </DataGrid>
         </TabItem>
      </TabControl>
   </Grid>
</Window>

A note on data types: You keep a member DataType in your DataSignalModel to determine its type. Usually you would create specialized types for each data and not a one-for-all model, e.g.:

  • BoolDataSignalModel
  • IntDataSignalModel
  • FloatDataSignalModel

This is also favorable from a design perspective to separate concerns. Those types might also expose unique properties, methods and events that do not apply to all variants of your data. Moreover, it simplifies bindings and templating a lot if you can depend on a concrete type.

thatguy
  • 21,059
  • 6
  • 30
  • 40
  • Thank you very much for this detailed answer, and solution. It works great. And thanks for the advice on the data type implementation. – NorwE Dec 12 '20 at 15:05