Универсализация ViewList в WPF для динамических атрибутов файлов
Полное руководство по созданию универсального ViewList в WPF для отображения файлов с неограниченными динамическими атрибутами. Реализация с MVVM и оптимизацией производительности.
Как универсализировать обычный ViewList в WPF C# XAML для отображения файлов с динамическими атрибутами?
Задача: группа файлов может содержать различные атрибуты (атрибуты задаются пользователем и их может быть неограниченное количество, универсализация по типам невозможна). Например:
Файл 1 Файл 2 Файл 3
atr1=1 atr2=5 atr1=7
atr3=4 atr4=19 atr=4
Как в обычном ViewList, который обычно используется в WPF, настроить отображение, и какой тип данных использовать для хранения такой информации?
Ожидаемый вид:
/ atr1 atr2 atr3 atr4
Файл 1 1 none 4 none
Файл 2 none 5 none 19
Файл 3 7 none none 4
Для создания универсального ViewList в WPF для отображения файлов с динамическими атрибутами требуется гибкий подход к структуре данных и механизму привязки. Вот полное решение:
Содержание
- Структура данных для хранения файлов с атрибутами
- Настройка XAML для DataGrid с динамическими столбцами
- Реализация динамического создания столбцов в коде
- MVVM подход с использованием поведений
- Пример полной реализации
- Рекомендации по оптимизации
Структура данных для хранения файлов с атрибутами
Для хранения информации о файлах с динамическими атрибутами рекомендуется использовать гибкие структуры данных. Вот несколько подходов:
Вариант 1: Использование словарей
public class FileInfo
{
public string FileName { get; set; }
public Dictionary<string, object> Attributes { get; set; } = new Dictionary<string, object>();
}
Вариант 2: Наследование от DynamicObject
public class DynamicFileInfo : DynamicObject
{
public string FileName { get; set; }
private readonly Dictionary<string, object> _attributes = new Dictionary<string, object>();
public override IEnumerable<string> GetDynamicMemberNames()
{
return _attributes.Keys;
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
return _attributes.TryGetValue(binder.Name, out result);
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
_attributes[binder.Name] = value;
return true;
}
}
Вариант 3: Observable коллекция для MVVM
public class ObservableFileInfo : INotifyPropertyChanged
{
private string _fileName;
private Dictionary<string, object> _attributes = new Dictionary<string, object>();
public string FileName
{
get => _fileName;
set { _fileName = value; OnPropertyChanged(); }
}
public Dictionary<string, object> Attributes
{
get => _attributes;
set { _attributes = value; OnPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Настройка XAML для DataGrid с динамическими столбцами
Основной XAML для DataGrid с отключенной автоматической генерацией столбцов:
<DataGrid x:Name="dataGridFiles"
AutoGenerateColumns="False"
ItemsSource="{Binding Files}"
CanUserAddRows="False"
GridLinesVisibility="Horizontal"
HeadersVisibility="All">
<!-- Столбец с именем файла -->
<DataGridTextColumn Header="/"
Binding="{Binding FileName}"
IsReadOnly="True"
Width="Auto"/>
<!-- Динамические столбцы для атрибутов будут добавлены программно -->
</DataGrid>
Реализация динамического создания столбцов в коде
Метод для создания столбцов на основе уникальных атрибутов
private void CreateDynamicColumns()
{
// Получаем все уникальные имена атрибутов
var allAttributeNames = new HashSet<string>();
foreach (var file in Files)
{
foreach (var attrName in file.Attributes.Keys)
{
allAttributeNames.Add(attrName);
}
}
// Очищаем существующие динамические столбцы (оставляя только столбец с именем файла)
var dynamicColumns = dataGridFiles.Columns
.Where(col => col.Header.ToString() != "/")
.ToList();
foreach (var column in dynamicColumns)
{
dataGridFiles.Columns.Remove(column);
}
// Создаем новые столбцы для каждого уникального атрибута
foreach (var attrName in allAttributeNames.OrderBy(name => name))
{
var column = new DataGridTextColumn
{
Header = attrName,
Binding = CreateAttributeBinding(attrName),
IsReadOnly = true,
Width = new DataGridLength(1, DataGridLengthUnitType.Star)
};
dataGridFiles.Columns.Add(column);
}
}
private Binding CreateAttributeBinding(string attributeName)
{
var binding = new Binding
{
Path = new PropertyPath($"Attributes[{attributeName}]"),
Converter = new AttributeValueConverter(),
FallbackValue = "none"
};
return binding;
}
Конвертер значений атрибутов
public class AttributeValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value?.ToString() ?? "none";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
MVVM подход с использованием поведений
Для более чистой MVVM архитектуры можно использовать подход с присоединенными свойствами:
public static class DataGridColumnsBehavior
{
public static readonly DependencyProperty BindableColumnsProperty =
DependencyProperty.RegisterAttached("BindableColumns",
typeof(ObservableCollection<DataGridColumn>),
typeof(DataGridColumnsBehavior),
new UIPropertyMetadata(null, OnBindableColumnsChanged));
public static ObservableCollection<DataGridColumn> GetBindableColumns(DependencyObject obj)
{
return (ObservableCollection<DataGridColumn>)obj.GetValue(BindableColumnsProperty);
}
public static void SetBindableColumns(DependencyObject obj, ObservableCollection<DataGridColumn> value)
{
obj.SetValue(BindableColumnsProperty, value);
}
private static void OnBindableColumnsChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var dataGrid = obj as DataGrid;
if (dataGrid == null) return;
var columns = e.NewValue as ObservableCollection<DataGridColumn>;
if (columns == null) return;
dataGrid.Columns.Clear();
foreach (var column in columns)
{
dataGrid.Columns.Add(column);
}
columns.CollectionChanged += (sender, args) =>
{
dataGrid.Columns.Clear();
foreach (var column in columns)
{
dataGrid.Columns.Add(column);
}
};
}
}
Использование в XAML:
<DataGrid local:DataGridColumnsBehavior.BindableColumns="{Binding DynamicColumns}"
AutoGenerateColumns="False"
ItemsSource="{Binding Files}">
<DataGrid.Columns>
<DataGridTextColumn Header="/"
Binding="{Binding FileName}"
IsReadOnly="True"/>
</DataGrid.Columns>
</DataGrid>
Пример полной реализации
Модель данных
public class FileViewModel : INotifyPropertyChanged
{
private ObservableCollection<ObservableFileInfo> _files;
public ObservableCollection<ObservableFileInfo> Files
{
get => _files;
set { _files = value; OnPropertyChanged(); }
}
public ObservableCollection<DataGridColumn> DynamicColumns { get; }
public FileViewModel()
{
DynamicColumns = new ObservableCollection<DataGridColumn>();
InitializeSampleData();
}
private void InitializeSampleData()
{
Files = new ObservableCollection<ObservableFileInfo>
{
new ObservableFileInfo
{
FileName = "Файл 1",
Attributes = new Dictionary<string, object>
{
{ "atr1", 1 },
{ "atr3", 4 }
}
},
new ObservableFileInfo
{
FileName = "Файл 2",
Attributes = new Dictionary<string, object>
{
{ "atr2", 5 },
{ "atr4", 19 }
}
},
new ObservableFileInfo
{
FileName = "Файл 3",
Attributes = new Dictionary<string, object>
{
{ "atr1", 7 },
{ "atr", 4 }
}
}
};
UpdateDynamicColumns();
}
public void UpdateDynamicColumns()
{
// Очищаем существующие динамические столбцы
var fileColumn = DynamicColumns.FirstOrDefault(col => col.Header.ToString() == "/");
DynamicColumns.Clear();
if (fileColumn != null)
DynamicColumns.Add(fileColumn);
// Получаем все уникальные атрибуты
var allAttributeNames = Files
.SelectMany(f => f.Attributes.Keys)
.Distinct()
.OrderBy(name => name);
// Создаем столбцы для каждого атрибута
foreach (var attrName in allAttributeNames)
{
var column = new DataGridTextColumn
{
Header = attrName,
Binding = new Binding($"Attributes[{attrName}]")
{
Converter = new AttributeValueConverter(),
FallbackValue = "none"
},
IsReadOnly = true,
Width = new DataGridLength(1, DataGridLengthUnitType.Star)
};
DynamicColumns.Add(column);
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
ViewModel в главном окне
public partial class MainWindow : Window
{
public FileViewModel ViewModel { get; }
public MainWindow()
{
InitializeComponent();
ViewModel = new FileViewModel();
DataContext = ViewModel;
}
}
Рекомендации по оптимизации
-
Кэширование уникальных атрибутов: Сохраняйте список уникальных атрибутов и обновляйте только при изменении набора файлов или атрибутов.
-
Отложенное создание столбцов: Для большого количества файлов откладывайте создание столбцов до момента отображения.
-
Виртуализация: Используйте
VirtualizingStackPanelдля улучшения производительности при большом количестве строк.
<DataGrid VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling">
-
Фильтрация и сортировка: Добавьте поддержку фильтрации и сортировки для удобства работы.
-
Обработка больших объемов данных: Для тысяч записей рассмотрите использование
PagedCollectionViewилиCollectionViewSource. -
Асинхронное обновление: Если данные обновляются часто, используйте асинхронные методы для обновления столбцов.
public async void UpdateDynamicColumnsAsync()
{
await Task.Run(() => UpdateDynamicColumns());
}
Этот подход позволяет полностью универсализировать отображение файлов с динамическими атрибутами в WPF DataGrid, обеспечивая гибкость и расширяемость при работе с неограниченным набором атрибутов.
Источники
- DataGrid with Dynamic Columns | Zeeshan Amjad’s Blog
- WPF Dynamically Generated DataGrid | Paul Stovell
- Dynamic Columns in a WPF DataGrid Control (Part 2) - CodeProject
- WPF dynamic columns and values for DataGrid - Stack Overflow
- WPF dynamic datagrid binding methods - Stack Overflow
- Filling a Datagrid with dynamic Columns - Stack Overflow
- DataGrid columns - The complete WPF tutorial
Заключение
- Для реализации универсального ViewList с динамическими атрибутами рекомендуется использовать
Dictionary<string, object>илиDynamicObjectдля хранения данных о файлах - Настройте DataGrid с
AutoGenerateColumns="False"и создайте столбцы программно на основе уникальных атрибутов - Используйте MVVM подход с ObservableCollection для динамического обновления интерфейса
- Реализуйте конвертер значений для отображения “none” вместо пустых значений
- Оптимизируйте производительность для работы с большими объемами данных через виртуализацию и асинхронные обновления
Это решение обеспечивает гибкость и масштабируемость для работы с неограниченным количеством атрибутов файлов в WPF приложениях.