DataGrid 或 GridView CellTemplate 中的动态内容

Posted

技术标签:

【中文标题】DataGrid 或 GridView CellTemplate 中的动态内容【英文标题】:Dynamic content in DataGrid or GridView CellTemplate 【发布时间】:2021-03-23 16:07:38 【问题描述】:

我正在尝试使用DataGrid 实现一个视图,以显示来自控制器的实时数据,并有可能允许将列表中的控件的数据写回控制器。应根据数据信号的数据类型动态选择控件。在我目前的方法中,如果数据类型是布尔值,我使用按钮,否则单元格中的控件应该是滑块。

问题是每个不同的控件我只能得到一个可见的实例。

我尝试使用 DataGrid 控件和带有 GridViewListView,正如您在 2 个选项卡中看到的那样:

如果我在GridView 上单击两次,控件将在所选行中可见,并在另一行中消失。在ListView 中,如果我单击任何没有可见控件的单元格,则不会发生任何事情。

我在这个例子中使用了Material Design In XAML Toolkit,但没有添加它我得到了相同的结果。

我想我无法在视图中使用CellTemplate,但我希望有办法让它工作。

代码:

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 DataSignalModelName = "Data Signal 0", Address = "Float Data Type Address", DataType = DataType.Float,
                new DataSignalModelName = "Data Signal 1", Address = "Bool Data Type Address", DataType = DataType.Bool,
                new DataSignalModelName = "Data Signal 2", Address = "Bool Data Type Address", DataType = DataType.Bool,
                new DataSignalModelName = "Data Signal 3", Address = "Bool Data Type Address", DataType = DataType.Bool,
                new DataSignalModelName = "Data Signal 4", Address = "Bool Data Type Address", DataType = DataType.Bool,
                new DataSignalModelName = "Data Signal 5", Address = "Float Data Type Address", DataType = DataType.Float,
                new DataSignalModelName = "Data Signal 6", Address = "Float Data Type Address", DataType = DataType.Float,
                new DataSignalModelName = "Data Signal 7", Address = "Bool Data Type Address", DataType = DataType.Bool,
                new DataSignalModelName = "Data Signal 8", Address = "Bool Data Type Address", DataType = DataType.Bool,
                new DataSignalModelName = "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();
        
    

【问题讨论】:

您是否遇到了 DataTriggers 不读取初始值的问题?尝试谷歌搜索 wpf datatriggeronload。 【参考方案1】:

您看到的行为是由您创建数据模板的方式引起的。 ContentControl 内部为每个单元格实例化,但样式设置器内部的控件没有。这意味着按钮和滑块的单个实例在所有单元格之间共享。由于 WPF 中的控件只能有一个父级 - 或者换句话说,它只能在可视化树中出现一次 - 它只能在单元格之间重新分配或移动。

为了解决问题,不要在样式中创建控件,而是在数据模板中创建控件,例如:

<!-- 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>

当您存储一个数据类型成员而不是创建两个不同的模型类型时,您必须创建一个自定义 DataTemplateSelector 以根据 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);
   

在资源字典中创建此转换器的实例,您也可以在其中存储数据模板。

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

然后,像这样调整ListViewDataGrid 中的列定义:

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

就是这样。数据模板选择器将自动确定适当的模板并在资源中搜索它。您可以从DataSignalModel 中删除IsDataTypeBool,因为它不是必需的。

为方便起见,这里是您的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>

关于数据类型的说明:您在DataSignalModel 中保留一个成员DataType 以确定其类型。通常你会为每个数据创建专门的类型,而不是一个单一的模型,例如:

BoolDataSignalModel IntDataSignalModel FloatDataSignalModel

从设计的角度来看,这也有利于分离关注点。这些类型还可能公开不适用于所有数据变体的独特属性、方法和事件。此外,如果您可以依赖具体类型,它会大大简化绑定和模板。

【讨论】:

非常感谢您提供如此详细的答案和解决方案。它工作得很好。并感谢有关数据类型实现的建议。

以上是关于DataGrid 或 GridView CellTemplate 中的动态内容的主要内容,如果未能解决你的问题,请参考以下文章

把datagrid转换成gridview

怎么获取datagrid中数据

C# 分页中 CodeBehind 的 GridView 不起作用

如何获取GridView其中一列的所有数据

easyui datagrid 加载两次请求,触发两次ajax 请求 问题

easyui的datagrid怎么绑定数据库