wpf datagrid colum实际晶体不正确 - 等待observableCollection完成更新

发布于 2025-02-09 20:59:55 字数 1075 浏览 2 评论 0原文

我有点问题。我有一个datagrid,其物品库是由观测值授予的。 (我正在创建一个小插件以进行修订)。

我的ObservableCollection包含一些在Revit进行选择时创建的元素。我的目标是创建一个带有文本框的stackpanel,其确实与已设置为自动的实际列的高度相同。

在以“正常”速度运行代码时,我会得到这样的结果:

当我调试整个过程时,它运行较慢,并且得到这样的结果(我想在没有调试模式的情况下获得):

popup.DataGridSelectedObjects.UpdateLayout();
popup.DataGridSelectedObjects.Items.Refresh();
Thread.Sleep(2000)

public void AddRegexItemToStackpanel(string parameterName, DataGridColumn dc)
{
    TextBox printTextBlock = new TextBox();
    printTextBlock.Width = dc.ActualWidth;
    printTextBlock.Margin = new Thickness(0.5, 0, 0, 0);
    printTextBlock.Name = dc.Header.ToString().Replace("__", "_");

    StackRegexPanel.Children.Add(printTextBlock);
}

是否有任何方法可以等待观察力授权正确更新?

非常感谢 詹妮斯

I´m kind of stuck here with a little problem. I´ve got a DataGrid whose ItemSource is fed by an ObservableCollection. (I´m creating a little plugin for Revit).

My ObservableCollection contains of some Elements which are created when doing a selection in Revit. It is my goal to create a Stackpanel with Textboxes, which do have the same height as the actualized Columnwidths, which are set to Auto.

When running the code in "normal" speed I get a result like this:
enter image description here

When I debug the whole process it runs slower and I get a result like this (Which I want to get without debug mode):
enter image description here

popup.DataGridSelectedObjects.UpdateLayout();
popup.DataGridSelectedObjects.Items.Refresh();
Thread.Sleep(2000)

public void AddRegexItemToStackpanel(string parameterName, DataGridColumn dc)
{
    TextBox printTextBlock = new TextBox();
    printTextBlock.Width = dc.ActualWidth;
    printTextBlock.Margin = new Thickness(0.5, 0, 0, 0);
    printTextBlock.Name = dc.Header.ToString().Replace("__", "_");

    StackRegexPanel.Children.Add(printTextBlock);
}

Is there any method to wait the Observablecollection to update properly?

Thanks a lot and kind regards,
Jannis

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

毁梦 2025-02-16 20:59:55

您应该使用其他方法。使用数据绑定应该是最直观的。避免使用XAML的XAML可能会使生活大部分时间都变得更加困难,并且您的代码会开始闻到。

您的thread.sleep看起来也非常可疑。还应避免调用updateLayout()和 items.refresh()以提高性能。

更改数据源(observableCollection)已经触发了刷新,并在必要时是完整的布局通行证。无需第二次触发这两个。它只会使您的UI慢。

在下面您找到两个示例:静态,硬编码版本和一个更优雅,更具动态的版本(在其中自动添加文本框以匹配列计数)。
如果调整了列的大小(使用数据绑定时,则两个示例将自动调整每个TextBox的宽度(作为奖励)。这些示例还强调了WPF中数据结合的效率。避免数据绑定和XAML时,代码总是会变得臭和过于复杂。

要制作textbox以遵循其各自列的宽度,只需使用数据绑定:

<StackPanel Orientation="Horizontal">
  <TextBox Width="{Binding ElementName=Table, Path=Columns[0].ActualWidth}"
           Margin="0.5,0,0,0" />
  <TextBox Width="{Binding ElementName=Table, Path=Columns[1].ActualWidth}"
           Margin="0.5,0,0,0" />
  <TextBox Width="{Binding ElementName=Table, Path=Columns[2].ActualWidth}" 
           Margin="0.5,0,0,0" />
</StackPanel>

<DataGrid x:Name="Table" />

为了使其动态,只需使用itemscontrol即可配置为水平显示其项目:

<ItemsControl ItemsSource="{Binding ElementName=Table, Path=Columns}">
  <ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
      <StackPanel Orientation="Horizontal" />
    </ItemsPanelTemplate>
  </ItemsControl.ItemsPanel>

  <ItemsControl.ItemTemplate>
    <DataTemplate DataType="{x:Type DataGridColumn}">
      <TextBox Width="{Binding ActualWidth}"
               Margin="0.5,0,0,0" />
    </DataTemplate>
  </ItemsControl.ItemTemplate>
</ItemsControl>

<DataGrid x:Name="Table" />

在这两种情况下,您都应实现ivalueconverter以从double转换为厚度(对于Margin)并绑定第一个示例的stackpanel.margin iteg> itemscontrol.margindatagrid.rowheaderacheractualwidth属性> 的属性属性>用于调整行header的属性(正确对齐文本框)。


因为您提供了更多信息,所以我认为需要删除或调整我的答案:

要动态更改列计数,您应该始终将DataTable用作数据源。我添加了一种扩展方法,该方法将集合转换为DataTable,以防万一。

由于textbox元素旨在根据其关联的列过滤,因此我建议修改datagrid.columnheaderstyle以将textbox添加到列标题。这将更加方便,因为textbox现在将自动调整和移动(如果列拖动)。

列的textbox将绑定到observableCollection of String> String> String VALUTION(filter Expressions),其中每个项目的索引直接映射到列索引。处理colectionChanged事件允许处理textbox输入。

mainwindow.xaml.cs

partial class MainWindow : Window
{public ObservableCollection<string> FilterExpressions
  {
    get => (ObservableCollection<string>)GetValue(FilterExpressionsProperty);
    set => SetValue(FilterExpressionsProperty, value);
  }

  public static readonly DependencyProperty FilterExpressionsProperty = DependencyProperty.Register(
    "FilterExpressions", 
    typeof(ObservableCollection<string>), 
    typeof(MainWindow), 
    new PropertyMetadata(default));

  public DataTable DataSource
  {
    get => (DataTable)GetValue(DataSourceProperty);
    set => SetValue(DataSourceProperty, value);
  }

  public static readonly DependencyProperty DataSourceProperty = DependencyProperty.Register(
    "DataSource", 
    typeof(DataTable), 
    typeof(MainWindow), 
    new PropertyMetadata(default));

  // Example data models to show 
  // how to convert the collection to a DataTable
  private List<User> Users { get; }

  public MainWindow()
  {
    InitializeComponent();

    this.FilterExpressions = new ObservableCollection<string>();
    this.FilterExpressions.CollectionChanged += OnFilterExpressionsChanged;
      
    this.Users = new List<User>();
    for (int index = 0; index < 500; index++)
    {
      this.Users.Add(new User());
    }

    this.DataSource = this.Users.ToDataTable();
  }

  private void OnFilterExpressionsChanged(object? sender, NotifyCollectionChangedEventArgs e)
  {
    string changedFilterExpression = e.NewItems.Cast<string>().First();
    int columnIndexOfchangedFilterExpression = e.NewStartingIndex;

    // TODO::Handle filter expressions
  }

  // Example on how to modify the column count
  private void AddColumnToDataTable()
  {
    // If only using a DataTable
    this.DataSource.Columns.Add(new DataColumn("Column name", typeof(string)));

    // If underlying data source is a collection, then
    // add new data models and call extension method ToDataTable
    var newItemsWithAdditionalProperties = new List<object>();
    this.DataSource = newItemsWithAdditionalProperties.ToDataTable();
  }
}

mainwindow.xaml
示例以显示如何修改列标题以添加textbox以及如何使用附件行为。 datagrid现在绑定到DataTable,以便动态添加/删除列。

<Window>
  <DataGrid ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataSource}"
            local:DataGridColumnFilter.FilterValues="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=FilterExpressions}">
    <DataGrid.Resources>
      <Style x:Key="ColumnHeaderGripperStyle"
              TargetType="{x:Type Thumb}">
        <Setter Property="Width"
                Value="8" />
        <Setter Property="Background"
                Value="Transparent" />
        <Setter Property="Cursor"
                Value="SizeWE" />
        <Setter Property="Template">
          <Setter.Value>
            <ControlTemplate TargetType="{x:Type Thumb}">
              <Border Background="{TemplateBinding Background}"
                      Padding="{TemplateBinding Padding}" />
            </ControlTemplate>
          </Setter.Value>
        </Setter>
      </Style>
    </DataGrid.Resources>

    <DataGrid.ColumnHeaderStyle>
      <Style TargetType="{x:Type DataGridColumnHeader}">
        <Setter Property="VerticalContentAlignment"
                Value="Center" />
        <Setter Property="Template">
          <Setter.Value>
            <ControlTemplate TargetType="{x:Type DataGridColumnHeader}">
              <Grid>
                <Border x:Name="columnHeaderBorder"
                        BorderThickness="1"
                        Padding="3,0,3,0">
                  <Border.BorderBrush>
                    <LinearGradientBrush EndPoint="0.5,1"
                                          StartPoint="0.5,0">
                      <GradientStop Color="LightGray"
                                    Offset="0" />
                      <GradientStop Color="DarkGray"
                                    Offset="1" />
                    </LinearGradientBrush>
                  </Border.BorderBrush>
                  <Border.Background>
                    <LinearGradientBrush EndPoint="0.5,1"
                                          StartPoint="0.5,0">
                      <GradientStop Color="wHITE"
                                    Offset="0" />
                      <GradientStop Color="SkyBlue"
                                    Offset="1" />
                    </LinearGradientBrush>
                  </Border.Background>
                  <StackPanel>
                    <TextBox x:Name="PART_FilterInput"
                              Width="{TemplateBinding Width}" />

                    <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                      SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
                                      VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
                  </StackPanel>
                </Border>

                <Thumb x:Name="PART_LeftHeaderGripper"
                        HorizontalAlignment="Left"
                        Style="{StaticResource ColumnHeaderGripperStyle}" />
                <Thumb x:Name="PART_RightHeaderGripper"
                        HorizontalAlignment="Right"
                        Style="{StaticResource ColumnHeaderGripperStyle}" />
              </Grid>
            </ControlTemplate>
          </Setter.Value>
        </Setter>
        <Setter Property="Background">
          <Setter.Value>
            <LinearGradientBrush EndPoint="0.5,1"
                                  StartPoint="0.5,0">
              <GradientStop Color="White"
                            Offset="0" />
              <GradientStop Color="DeepSkyBlue"
                            Offset="1" />
            </LinearGradientBrush>
          </Setter.Value>
        </Setter>
      </Style>
    </DataGrid.ColumnHeaderStyle>
  </DataGrid>
</Window>

datagridcolumnfilter.cs
附加行为以映射textbox列标题的元素到集合(数据源) - 方向是一种方式,更新是在textbox.lostfocus上发送的。

我的建议是扩展datagrid,以摆脱这种附带的行为,并为控制处理增加更多便利性。

如果仅与视图相关(通常是这种情况),即您不打算根据过滤修改数据源,我建议将过滤逻辑移动到附加的行为(或扩展datagrid)。这将使您的模型保持清洁。

public class DataGridColumnFilter : DependencyObject
{
  public static IList<string> GetFilterValues(DependencyObject obj) => (IList<string>)obj.GetValue(FilterValuesProperty);
  public static void SetFilterValues(DependencyObject obj, IList<string> value) => obj.SetValue(FilterValuesProperty, value);

  public static readonly DependencyProperty FilterValuesProperty = DependencyProperty.RegisterAttached(
    "FilterValues",
    typeof(IList<string>),
    typeof(DataGridColumnFilter),
    new PropertyMetadata(default(IList<string>), OnFilterValuesChanged));

  private static Dictionary<TextBox, int> GetIndexMap(DependencyObject obj) => (Dictionary<TextBox, int>)obj.GetValue(IndexMapProperty);
  private static void SetIndexMap(DependencyObject obj, Dictionary<TextBox, int> value) => obj.SetValue(IndexMapProperty, value);

  private static readonly DependencyProperty IndexMapProperty = DependencyProperty.RegisterAttached(
    "IndexMap",
    typeof(Dictionary<TextBox, int>),
    typeof(DataGridColumnFilter),
    new PropertyMetadata(default));

  private static Dictionary<DataGridColumnHeadersPresenter, DataGrid> DataGridMap { get; } = new Dictionary<DataGridColumnHeadersPresenter, DataGrid>();

  private static void OnFilterValuesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
    if (d is not DataGrid dataGrid)
    {
      return;
    }

    if (!dataGrid.IsLoaded)
    {
      dataGrid.Loaded += OnDataGridColumnsGenerated;
    }
    else
    {
      Initialize(dataGrid);
    }
  }

  private static void OnDataGridColumnsGenerated(object? sender, EventArgs e)
  {
    var dataGrid = sender as DataGrid;
    Initialize(dataGrid);
  }

  private static void Initialize(DataGrid dataGrid)
  {
    DataGridColumnHeadersPresenter headerPresenter = dataGrid.FindVisualChildren<DataGridColumnHeadersPresenter>().First();
    DataGridMap.TryAdd(headerPresenter, dataGrid);
    headerPresenter.AddHandler(UIElement.LostFocusEvent, new RoutedEventHandler(OnTextBoxLostFocus));
    DataGridCellsPanel cellsPanel = headerPresenter.FindVisualChildren<DataGridCellsPanel>().First();
    IEnumerable<TextBox> inputFields = cellsPanel.FindVisualChildren<TextBox>();
    RegisterColumnHeaderTextBoxes(dataGrid, inputFields);
  }

  private static void RegisterColumnHeaderTextBoxes(DataGrid dataGrid, IEnumerable<TextBox> inputFields)
  {
    var indexMap = new Dictionary<TextBox, int>();
    SetIndexMap(dataGrid, indexMap);
    for (int index = 0; index < inputFields.Count(); index++)
    {
      TextBox inputField = inputFields.ElementAt(index);
      indexMap.Add(inputField, index);
    }
  }

  private static void OnTextBoxLostFocus(object sender, RoutedEventArgs e)
  {
    if (e.OriginalSource is not TextBox textInputField)
    {
      return;
    }

    var headerPresenter = sender as DataGridColumnHeadersPresenter;
    if (!DataGridMap.TryGetValue(headerPresenter, out DataGrid host))
    {
      return;
    }

    IList<string> filterExpressionSource = GetFilterValues(host);
    Dictionary<TextBox, int> indexMap = GetIndexMap(host);
    int columnIndex = indexMap[textInputField];

    // Preload source collection if empty
    if (columnIndex >= filterExpressionSource.Count)
    {
      for (int index = filterExpressionSource.Count; index <= columnIndex; index++)
      {
        filterExpressionSource.Add(string.Empty);
      }
    }

    UpdateFilterExpressionSource(textInputField, filterExpressionSource, columnIndex);
  }

  private static void UpdateFilterExpressionSource(TextBox textInputField, IList<string> filterExpressionSource, int columnIndex)
  {
    if (!filterExpressionSource[columnIndex].Equals(textInputField.Text, StringComparison.Ordinal))
    {
      filterExpressionSource[columnIndex] = textInputField.Text;
    }
  }
}

ExtensionMethods.cs
扩展方法将iEnumerable&lt; tdata&gt;转换为datatable

public static class ExtensionMethods
{
  public static DataTable ToDataTable<TData>(this IEnumerable<TData> source)
  {
    Type dataType = typeof(TData);
    IEnumerable<PropertyInfo> publicPropertyInfos = dataType.GetProperties()
      .Where(propertyInfo => propertyInfo.GetCustomAttribute<IgnoreAttribute>() is null);
    var result = new DataTable();
    var columnNameMapping = new Dictionary<string, string>();
    foreach (PropertyInfo publicPropertyInfo in publicPropertyInfos)
    {
      DataColumn newColumn = result.Columns.Add(publicPropertyInfo.Name, publicPropertyInfo.PropertyType);

      System.ComponentModel.DisplayNameAttribute displayNameAttribute = publicPropertyInfo.GetCustomAttribute<System.ComponentModel.DisplayNameAttribute>();
      if (displayNameAttribute is not null)
      {
        newColumn.ColumnName = displayNameAttribute.DisplayName;
      }

      columnNameMapping.Add(publicPropertyInfo.Name, newColumn.ColumnName);
    }

    foreach (TData rowData in source)
    {
      DataRow newRow = result.NewRow();
      result.Rows.Add(newRow);
      foreach (PropertyInfo publicPropertyInfo in publicPropertyInfos)
      {
        object? columnValue = publicPropertyInfo.GetValue(rowData);
        string columnName = columnNameMapping[publicPropertyInfo.Name];
        newRow[columnName] = columnValue;
      }
    }

    return result;
  }
}

user.cs
用于创建dataTable的数据模型在上面的示例中( mainwindow.xaml.cs )。
该类还提供了一个有关如何使用属性ignoreatTribute控制
的示例
属性/列的可见性和system.com.ponentmodel.displayname以重命名属性/列。

public class User : INotifyPropertyChanged
{
  public string UserName { get; set; }

  // Assign new column name for this property
  [System.ComponentModel.DisplayName("Mail address")]
  public string UserMail { get; set; }

  // Don't add this property as column to the DataTable
  [Ignore]
  public int Age { get; set; }
}

nighoreatTribute.cs

[AttributeUsage(AttributeTargets.All, Inherited = false)]
public class IgnoreAttribute : Attribute
{
}

You should use a different approach. Using data binding should be the most intuitive. Avoiding XAML where XAML is possible will make life a lot harder most of the time and your code will start to smell.

Your Thread.Sleep looks very very suspicious too. It's also very likely that calling UpdateLayout() and Items.Refresh() should be avoided to improve the performance.

Changing the data source (ObservableCollection) will already trigger a refresh and, if necessary, a complete layout pass. No need to trigger both a second time. It will only make your UI slow.

Below you find two examples: a static, hard-coded version and a more elegant and dynamic version (where text boxes are added automatically to match the column count).
Both examples will automatically adjust the width of each TextBox if columns are resized (as a bonus when using data binding). The examples also highlight the efficiency of data binding in WPF. Code will always become smelly and overly complex when avoiding data binding and XAML.

To make a TextBox to follow the width of its respective column, simply use data binding:

<StackPanel Orientation="Horizontal">
  <TextBox Width="{Binding ElementName=Table, Path=Columns[0].ActualWidth}"
           Margin="0.5,0,0,0" />
  <TextBox Width="{Binding ElementName=Table, Path=Columns[1].ActualWidth}"
           Margin="0.5,0,0,0" />
  <TextBox Width="{Binding ElementName=Table, Path=Columns[2].ActualWidth}" 
           Margin="0.5,0,0,0" />
</StackPanel>

<DataGrid x:Name="Table" />

And to make it dynamic, simply use an ItemsControl that is configured to display its items horizontally:

<ItemsControl ItemsSource="{Binding ElementName=Table, Path=Columns}">
  <ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
      <StackPanel Orientation="Horizontal" />
    </ItemsPanelTemplate>
  </ItemsControl.ItemsPanel>

  <ItemsControl.ItemTemplate>
    <DataTemplate DataType="{x:Type DataGridColumn}">
      <TextBox Width="{Binding ActualWidth}"
               Margin="0.5,0,0,0" />
    </DataTemplate>
  </ItemsControl.ItemTemplate>
</ItemsControl>

<DataGrid x:Name="Table" />

In both cases you should implement an IValueConverter to convert from double to Thickness (for the Margin) and bind the StackPanel.Margin of the first example or the ItemsControl.Margin of the second example to the DataGrid.RowHeaderActualWidth property to adjust for the row header (to align the text boxes properly).


Because you have provided more information, I felt the need to either delete or adjust my answer:

To dynamically change column count, you should always use a DataTable as data source. I have added an extension method that converts a collection to a DataTable, in case you need it.

Since the TextBox elements are meant to filter based on their associated column, I would suggest to modify the DataGrid.ColumnHeaderStyle to add a TextBox to the column header. This will be more convenient as the TextBox will now automatically resize and move (in case the column is dragged).

The column's TextBox will bind to a ObservableCollection of string values (filter expressions), where the index of each item maps directly to a column index. Handling the ColectionChanged event allows to handle the TextBox input.

MainWindow.xaml.cs

partial class MainWindow : Window
{public ObservableCollection<string> FilterExpressions
  {
    get => (ObservableCollection<string>)GetValue(FilterExpressionsProperty);
    set => SetValue(FilterExpressionsProperty, value);
  }

  public static readonly DependencyProperty FilterExpressionsProperty = DependencyProperty.Register(
    "FilterExpressions", 
    typeof(ObservableCollection<string>), 
    typeof(MainWindow), 
    new PropertyMetadata(default));

  public DataTable DataSource
  {
    get => (DataTable)GetValue(DataSourceProperty);
    set => SetValue(DataSourceProperty, value);
  }

  public static readonly DependencyProperty DataSourceProperty = DependencyProperty.Register(
    "DataSource", 
    typeof(DataTable), 
    typeof(MainWindow), 
    new PropertyMetadata(default));

  // Example data models to show 
  // how to convert the collection to a DataTable
  private List<User> Users { get; }

  public MainWindow()
  {
    InitializeComponent();

    this.FilterExpressions = new ObservableCollection<string>();
    this.FilterExpressions.CollectionChanged += OnFilterExpressionsChanged;
      
    this.Users = new List<User>();
    for (int index = 0; index < 500; index++)
    {
      this.Users.Add(new User());
    }

    this.DataSource = this.Users.ToDataTable();
  }

  private void OnFilterExpressionsChanged(object? sender, NotifyCollectionChangedEventArgs e)
  {
    string changedFilterExpression = e.NewItems.Cast<string>().First();
    int columnIndexOfchangedFilterExpression = e.NewStartingIndex;

    // TODO::Handle filter expressions
  }

  // Example on how to modify the column count
  private void AddColumnToDataTable()
  {
    // If only using a DataTable
    this.DataSource.Columns.Add(new DataColumn("Column name", typeof(string)));

    // If underlying data source is a collection, then
    // add new data models and call extension method ToDataTable
    var newItemsWithAdditionalProperties = new List<object>();
    this.DataSource = newItemsWithAdditionalProperties.ToDataTable();
  }
}

MainWindow.xaml
Example to show how to modify the column header to add a TextBox and how to use the attached behavior. The DataGrid now binds to a DataTable in order to allow to add/remove columns dynamically.

<Window>
  <DataGrid ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataSource}"
            local:DataGridColumnFilter.FilterValues="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=FilterExpressions}">
    <DataGrid.Resources>
      <Style x:Key="ColumnHeaderGripperStyle"
              TargetType="{x:Type Thumb}">
        <Setter Property="Width"
                Value="8" />
        <Setter Property="Background"
                Value="Transparent" />
        <Setter Property="Cursor"
                Value="SizeWE" />
        <Setter Property="Template">
          <Setter.Value>
            <ControlTemplate TargetType="{x:Type Thumb}">
              <Border Background="{TemplateBinding Background}"
                      Padding="{TemplateBinding Padding}" />
            </ControlTemplate>
          </Setter.Value>
        </Setter>
      </Style>
    </DataGrid.Resources>

    <DataGrid.ColumnHeaderStyle>
      <Style TargetType="{x:Type DataGridColumnHeader}">
        <Setter Property="VerticalContentAlignment"
                Value="Center" />
        <Setter Property="Template">
          <Setter.Value>
            <ControlTemplate TargetType="{x:Type DataGridColumnHeader}">
              <Grid>
                <Border x:Name="columnHeaderBorder"
                        BorderThickness="1"
                        Padding="3,0,3,0">
                  <Border.BorderBrush>
                    <LinearGradientBrush EndPoint="0.5,1"
                                          StartPoint="0.5,0">
                      <GradientStop Color="LightGray"
                                    Offset="0" />
                      <GradientStop Color="DarkGray"
                                    Offset="1" />
                    </LinearGradientBrush>
                  </Border.BorderBrush>
                  <Border.Background>
                    <LinearGradientBrush EndPoint="0.5,1"
                                          StartPoint="0.5,0">
                      <GradientStop Color="wHITE"
                                    Offset="0" />
                      <GradientStop Color="SkyBlue"
                                    Offset="1" />
                    </LinearGradientBrush>
                  </Border.Background>
                  <StackPanel>
                    <TextBox x:Name="PART_FilterInput"
                              Width="{TemplateBinding Width}" />

                    <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                      SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
                                      VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
                  </StackPanel>
                </Border>

                <Thumb x:Name="PART_LeftHeaderGripper"
                        HorizontalAlignment="Left"
                        Style="{StaticResource ColumnHeaderGripperStyle}" />
                <Thumb x:Name="PART_RightHeaderGripper"
                        HorizontalAlignment="Right"
                        Style="{StaticResource ColumnHeaderGripperStyle}" />
              </Grid>
            </ControlTemplate>
          </Setter.Value>
        </Setter>
        <Setter Property="Background">
          <Setter.Value>
            <LinearGradientBrush EndPoint="0.5,1"
                                  StartPoint="0.5,0">
              <GradientStop Color="White"
                            Offset="0" />
              <GradientStop Color="DeepSkyBlue"
                            Offset="1" />
            </LinearGradientBrush>
          </Setter.Value>
        </Setter>
      </Style>
    </DataGrid.ColumnHeaderStyle>
  </DataGrid>
</Window>

DataGridColumnFilter.cs
Attached behavior to map the TextBox elements of the column headers to a collection (data source) - direction is one way and update is send on TextBox.LostFocus.

My recommendation is to extend DataGrid to get rid of this attached behavior and to add more convenience to the control handling.

If filtering is only view related (which is usually the case) i.e. you don't intend to modify the data source based on the filtering, I recommend to move the filtering logic to the attached behavior (or extended DataGrid). This will keep your models clean.

public class DataGridColumnFilter : DependencyObject
{
  public static IList<string> GetFilterValues(DependencyObject obj) => (IList<string>)obj.GetValue(FilterValuesProperty);
  public static void SetFilterValues(DependencyObject obj, IList<string> value) => obj.SetValue(FilterValuesProperty, value);

  public static readonly DependencyProperty FilterValuesProperty = DependencyProperty.RegisterAttached(
    "FilterValues",
    typeof(IList<string>),
    typeof(DataGridColumnFilter),
    new PropertyMetadata(default(IList<string>), OnFilterValuesChanged));

  private static Dictionary<TextBox, int> GetIndexMap(DependencyObject obj) => (Dictionary<TextBox, int>)obj.GetValue(IndexMapProperty);
  private static void SetIndexMap(DependencyObject obj, Dictionary<TextBox, int> value) => obj.SetValue(IndexMapProperty, value);

  private static readonly DependencyProperty IndexMapProperty = DependencyProperty.RegisterAttached(
    "IndexMap",
    typeof(Dictionary<TextBox, int>),
    typeof(DataGridColumnFilter),
    new PropertyMetadata(default));

  private static Dictionary<DataGridColumnHeadersPresenter, DataGrid> DataGridMap { get; } = new Dictionary<DataGridColumnHeadersPresenter, DataGrid>();

  private static void OnFilterValuesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
    if (d is not DataGrid dataGrid)
    {
      return;
    }

    if (!dataGrid.IsLoaded)
    {
      dataGrid.Loaded += OnDataGridColumnsGenerated;
    }
    else
    {
      Initialize(dataGrid);
    }
  }

  private static void OnDataGridColumnsGenerated(object? sender, EventArgs e)
  {
    var dataGrid = sender as DataGrid;
    Initialize(dataGrid);
  }

  private static void Initialize(DataGrid dataGrid)
  {
    DataGridColumnHeadersPresenter headerPresenter = dataGrid.FindVisualChildren<DataGridColumnHeadersPresenter>().First();
    DataGridMap.TryAdd(headerPresenter, dataGrid);
    headerPresenter.AddHandler(UIElement.LostFocusEvent, new RoutedEventHandler(OnTextBoxLostFocus));
    DataGridCellsPanel cellsPanel = headerPresenter.FindVisualChildren<DataGridCellsPanel>().First();
    IEnumerable<TextBox> inputFields = cellsPanel.FindVisualChildren<TextBox>();
    RegisterColumnHeaderTextBoxes(dataGrid, inputFields);
  }

  private static void RegisterColumnHeaderTextBoxes(DataGrid dataGrid, IEnumerable<TextBox> inputFields)
  {
    var indexMap = new Dictionary<TextBox, int>();
    SetIndexMap(dataGrid, indexMap);
    for (int index = 0; index < inputFields.Count(); index++)
    {
      TextBox inputField = inputFields.ElementAt(index);
      indexMap.Add(inputField, index);
    }
  }

  private static void OnTextBoxLostFocus(object sender, RoutedEventArgs e)
  {
    if (e.OriginalSource is not TextBox textInputField)
    {
      return;
    }

    var headerPresenter = sender as DataGridColumnHeadersPresenter;
    if (!DataGridMap.TryGetValue(headerPresenter, out DataGrid host))
    {
      return;
    }

    IList<string> filterExpressionSource = GetFilterValues(host);
    Dictionary<TextBox, int> indexMap = GetIndexMap(host);
    int columnIndex = indexMap[textInputField];

    // Preload source collection if empty
    if (columnIndex >= filterExpressionSource.Count)
    {
      for (int index = filterExpressionSource.Count; index <= columnIndex; index++)
      {
        filterExpressionSource.Add(string.Empty);
      }
    }

    UpdateFilterExpressionSource(textInputField, filterExpressionSource, columnIndex);
  }

  private static void UpdateFilterExpressionSource(TextBox textInputField, IList<string> filterExpressionSource, int columnIndex)
  {
    if (!filterExpressionSource[columnIndex].Equals(textInputField.Text, StringComparison.Ordinal))
    {
      filterExpressionSource[columnIndex] = textInputField.Text;
    }
  }
}

ExtensionMethods.cs
Extension method to convert a IEnumerable<TData> to a DataTable.

public static class ExtensionMethods
{
  public static DataTable ToDataTable<TData>(this IEnumerable<TData> source)
  {
    Type dataType = typeof(TData);
    IEnumerable<PropertyInfo> publicPropertyInfos = dataType.GetProperties()
      .Where(propertyInfo => propertyInfo.GetCustomAttribute<IgnoreAttribute>() is null);
    var result = new DataTable();
    var columnNameMapping = new Dictionary<string, string>();
    foreach (PropertyInfo publicPropertyInfo in publicPropertyInfos)
    {
      DataColumn newColumn = result.Columns.Add(publicPropertyInfo.Name, publicPropertyInfo.PropertyType);

      System.ComponentModel.DisplayNameAttribute displayNameAttribute = publicPropertyInfo.GetCustomAttribute<System.ComponentModel.DisplayNameAttribute>();
      if (displayNameAttribute is not null)
      {
        newColumn.ColumnName = displayNameAttribute.DisplayName;
      }

      columnNameMapping.Add(publicPropertyInfo.Name, newColumn.ColumnName);
    }

    foreach (TData rowData in source)
    {
      DataRow newRow = result.NewRow();
      result.Rows.Add(newRow);
      foreach (PropertyInfo publicPropertyInfo in publicPropertyInfos)
      {
        object? columnValue = publicPropertyInfo.GetValue(rowData);
        string columnName = columnNameMapping[publicPropertyInfo.Name];
        newRow[columnName] = columnValue;
      }
    }

    return result;
  }
}

User.cs
The data model used to create the DataTable from in the above example (MainWindow.xaml.cs).
The class also gives an example on how to use the attributes IgnoreAttribute to control
the visibility of properties/columns and System.ComponentModel.DisplayName to rename the property/column.

public class User : INotifyPropertyChanged
{
  public string UserName { get; set; }

  // Assign new column name for this property
  [System.ComponentModel.DisplayName("Mail address")]
  public string UserMail { get; set; }

  // Don't add this property as column to the DataTable
  [Ignore]
  public int Age { get; set; }
}

IgnoreAttribute.cs

[AttributeUsage(AttributeTargets.All, Inherited = false)]
public class IgnoreAttribute : Attribute
{
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文