Reputation: 51
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:
When I debug the whole process it runs slower and I get a result like this (Which I want to get without debug mode):
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
Upvotes: -2
Views: 220
Reputation: 29028
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
{
}
Upvotes: 1