Другое

Универсализация 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 для отображения файлов с динамическими атрибутами требуется гибкий подход к структуре данных и механизму привязки. Вот полное решение:

Содержание

Структура данных для хранения файлов с атрибутами

Для хранения информации о файлах с динамическими атрибутами рекомендуется использовать гибкие структуры данных. Вот несколько подходов:

Вариант 1: Использование словарей

csharp
public class FileInfo
{
    public string FileName { get; set; }
    public Dictionary<string, object> Attributes { get; set; } = new Dictionary<string, object>();
}

Вариант 2: Наследование от DynamicObject

csharp
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

csharp
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 с отключенной автоматической генерацией столбцов:

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

Реализация динамического создания столбцов в коде

Метод для создания столбцов на основе уникальных атрибутов

csharp
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;
}

Конвертер значений атрибутов

csharp
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 архитектуры можно использовать подход с присоединенными свойствами:

csharp
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:

xml
<DataGrid local:DataGridColumnsBehavior.BindableColumns="{Binding DynamicColumns}"
          AutoGenerateColumns="False"
          ItemsSource="{Binding Files}">
    
    <DataGrid.Columns>
        <DataGridTextColumn Header="/" 
                            Binding="{Binding FileName}" 
                            IsReadOnly="True"/>
    </DataGrid.Columns>
</DataGrid>

Пример полной реализации

Модель данных

csharp
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 в главном окне

csharp
public partial class MainWindow : Window
{
    public FileViewModel ViewModel { get; }

    public MainWindow()
    {
        InitializeComponent();
        ViewModel = new FileViewModel();
        DataContext = ViewModel;
    }
}

Рекомендации по оптимизации

  1. Кэширование уникальных атрибутов: Сохраняйте список уникальных атрибутов и обновляйте только при изменении набора файлов или атрибутов.

  2. Отложенное создание столбцов: Для большого количества файлов откладывайте создание столбцов до момента отображения.

  3. Виртуализация: Используйте VirtualizingStackPanel для улучшения производительности при большом количестве строк.

xml
<DataGrid VirtualizingStackPanel.IsVirtualizing="True"
          VirtualizingStackPanel.VirtualizationMode="Recycling">
  1. Фильтрация и сортировка: Добавьте поддержку фильтрации и сортировки для удобства работы.

  2. Обработка больших объемов данных: Для тысяч записей рассмотрите использование PagedCollectionView или CollectionViewSource.

  3. Асинхронное обновление: Если данные обновляются часто, используйте асинхронные методы для обновления столбцов.

csharp
public async void UpdateDynamicColumnsAsync()
{
    await Task.Run(() => UpdateDynamicColumns());
}

Этот подход позволяет полностью универсализировать отображение файлов с динамическими атрибутами в WPF DataGrid, обеспечивая гибкость и расширяемость при работе с неограниченным набором атрибутов.

Источники

  1. DataGrid with Dynamic Columns | Zeeshan Amjad’s Blog
  2. WPF Dynamically Generated DataGrid | Paul Stovell
  3. Dynamic Columns in a WPF DataGrid Control (Part 2) - CodeProject
  4. WPF dynamic columns and values for DataGrid - Stack Overflow
  5. WPF dynamic datagrid binding methods - Stack Overflow
  6. Filling a Datagrid with dynamic Columns - Stack Overflow
  7. DataGrid columns - The complete WPF tutorial

Заключение

  • Для реализации универсального ViewList с динамическими атрибутами рекомендуется использовать Dictionary<string, object> или DynamicObject для хранения данных о файлах
  • Настройте DataGrid с AutoGenerateColumns="False" и создайте столбцы программно на основе уникальных атрибутов
  • Используйте MVVM подход с ObservableCollection для динамического обновления интерфейса
  • Реализуйте конвертер значений для отображения “none” вместо пустых значений
  • Оптимизируйте производительность для работы с большими объемами данных через виртуализацию и асинхронные обновления

Это решение обеспечивает гибкость и масштабируемость для работы с неограниченным количеством атрибутов файлов в WPF приложениях.

Авторы
Проверено модерацией
Модерация