DataGrid 或 GridView CellTemplate 中的动态内容
Dynamic content in DataGrid or GridView CellTemplate
我正在尝试实现一个带有 DataGrid
的视图以显示来自控制器的实时数据,并有可能将数据从列表中的控件写回控制器。应根据数据信号的数据类型动态选择控件。在我目前的方法中,如果数据类型是布尔值,我会使用一个按钮,否则单元格中的控件应该是一个滑块。
问题是我只获得了每个不同控件的一个可见实例。
我尝试使用 DataGrid
控件和带有 GridView
的 ListView
,如您在两个选项卡中所见:
如果我在 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 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();
}
}
}
您看到的行为是由您创建数据模板的方式造成的。里面的ContentControl
是为每个cell实例化的,但是styles的setter里面的控件没有。这意味着按钮和滑块的单个实例在所有单元格之间共享。由于 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"/>
然后,像这样调整 ListView
和 DataGrid
中的列定义:
<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
的视图以显示来自控制器的实时数据,并有可能将数据从列表中的控件写回控制器。应根据数据信号的数据类型动态选择控件。在我目前的方法中,如果数据类型是布尔值,我会使用一个按钮,否则单元格中的控件应该是一个滑块。
问题是我只获得了每个不同控件的一个可见实例。
我尝试使用 DataGrid
控件和带有 GridView
的 ListView
,如您在两个选项卡中所见:
如果我在 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 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();
}
}
}
您看到的行为是由您创建数据模板的方式造成的。里面的ContentControl
是为每个cell实例化的,但是styles的setter里面的控件没有。这意味着按钮和滑块的单个实例在所有单元格之间共享。由于 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"/>
然后,像这样调整 ListView
和 DataGrid
中的列定义:
<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
从设计的角度来看,这也有利于分离关注点。这些类型还可能公开不适用于所有数据变体的独特属性、方法和事件。此外,如果您可以依赖具体类型,它会大大简化绑定和模板化。