Другое

Как создать универсальный ListView в WPF для динамических атрибутов файлов

Полное руководство по созданию универсального ListView в WPF C# XAML для отображения файлов с произвольными атрибутами. Реализация с ExpandoObject и динамическими колонками.

Как создать универсальный ListView в WPF C# XAML для отображения файлов с динамическими атрибутами?

Задача: Группа файлов может содержать различные атрибуты (количество и типы атрибутов определяются пользователем и могут быть произвольными). Например:

Файл 1 Файл 2 Файл 3
atr1=1 atr2=5 atr1=7
atr3=4 atr4=19 atr=4

Как настроить ListView в WPF для отображения таких данных, и какой тип данных лучше использовать для хранения информации о файлах с динамическими атрибутами?

Ожидаемый результат отображения:
/ atr1 atr2 atr3 atr4
Файл 1 1 none 4 none
Файл 2 none 5 none 19
Файл 3 7 none none 4

Для создания универсального ListView в WPF для отображения файлов с динамическими атрибутами необходимо использовать гибкий подход к хранению данных и динамическому созданию колонок. Лучше всего подходит комбинация ObservableCollection с динамическими GridView колонками, создаваемыми через конвертер.

Содержание


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

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

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

csharp
public class FileItem
{
    public string Name { get; set; }
    public dynamic Attributes { get; set; }
    
    public FileItem(string name)
    {
        Name = name;
        Attributes = new ExpandoObject();
    }
}

Вариант 2: Использование Dictionary<string, object>

csharp
public class FileItem
{
    public string Name { get; set; }
    public Dictionary<string, object> Attributes { get; set; }
    
    public FileItem(string name)
    {
        Name = name;
        Attributes = new Dictionary<string, object>();
    }
}

Вариант 3: Observable коллекция

csharp
public ObservableCollection<FileItem> Files { get; set; } = new ObservableCollection<FileItem>();

Реализация XAML с динамическими колонками

Основной подход использует конвертер для создания GridView колонок на основе доступных атрибутов:

xml
<Window x:Class="DynamicFilesViewer.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DynamicFilesViewer"
        Title="Dynamic Files Viewer" Height="450" Width="800">
    
    <Window.Resources>
        <local:AttributesToColumnsConverter x:Key="AttributesToColumnsConverter"/>
    </Window.Resources>
    
    <Grid>
        <ListView ItemsSource="{Binding Files}" 
                  HorizontalContentAlignment="Stretch">
            <ListView.View>
                <GridView>
                    <!-- Колонка с именем файла -->
                    <GridViewColumn Header="Файл" 
                                    DisplayMemberBinding="{Binding Name}" 
                                    Width="100"/>
                    
                    <!-- Динамические колонки с атрибутами -->
                    <GridViewColumn Header="{Binding AllAttributes[0]}" 
                                    DisplayMemberBinding="{Binding Attributes.[atr1]}" 
                                    Width="80"/>
                    <GridViewColumn Header="{Binding AllAttributes[1]}" 
                                    DisplayMemberBinding="{Binding Attributes.[atr2]}" 
                                    Width="80"/>
                    <!-- Дополнительные колонки добавляются динамически -->
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>

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

xml
<ListView ItemsSource="{Binding Files}" 
          View="{Binding ColumnConfig, Converter={StaticResource ConfigToDynamicGridViewConverter}}"/>

Код для создания динамических колонок

Конвертер для создания динамических колонок

csharp
public class AttributesToColumnsConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var gridView = new GridView();
        
        // Добавляем колонку с именем файла
        gridView.Columns.Add(new GridViewColumn
        {
            Header = "Файл",
            DisplayMemberBinding = new Binding("Name"),
            Width = 100
        });
        
        // Получаем все уникальные атрибуты
        var allAttributes = GetUniqueAttributes(value as IEnumerable<FileItem>);
        
        foreach (var attribute in allAttributes)
        {
            gridView.Columns.Add(new GridViewColumn
            {
                Header = attribute,
                DisplayMemberBinding = new Binding($"Attributes.[{attribute}]"),
                Width = 80
            });
        }
        
        return gridView;
    }
    
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
    
    private List<string> GetUniqueAttributes(IEnumerable<FileItem> files)
    {
        if (files == null) return new List<string>();
        
        var attributes = new HashSet<string>();
        foreach (var file in files)
        {
            if (file.Attributes is Dictionary<string, object> dict)
            {
                foreach (var key in dict.Keys)
                {
                    attributes.Add(key);
                }
            }
            else if (file.Attributes is ExpandoObject expando)
            {
                foreach (var key in ((IDictionary<string, object>)expando).Keys)
                {
                    attributes.Add(key);
                }
            }
        }
        return attributes.OrderBy(a => a).ToList();
    }
}

Класс для демонстрации

csharp
public class FileItem
{
    public string Name { get; set; }
    public dynamic Attributes { get; set; }
    
    public FileItem(string name)
    {
        Name = name;
        Attributes = new ExpandoObject();
    }
}

public class MainViewModel : INotifyPropertyChanged
{
    public ObservableCollection<FileItem> Files { get; set; }
    
    public MainViewModel()
    {
        Files = new ObservableCollection<FileItem>();
        
        // Создаем тестовые данные
        var file1 = new FileItem("Файл 1");
        file1.Attributes.atr1 = 1;
        file1.Attributes.atr3 = 4;
        Files.Add(file1);
        
        var file2 = new FileItem("Файл 2");
        file2.Attributes.atr2 = 5;
        file2.Attributes.atr4 = 19;
        Files.Add(file2);
        
        var file3 = new FileItem("Файл 3");
        file3.Attributes.atr1 = 7;
        file3.Attributes.atr = 4;
        Files.Add(file3);
    }
    
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

MVVM подход

Для полноценного MVVM подхода можно создать ViewModel, которая будет управлять динамическими колонками:

csharp
public class FileViewModel : INotifyPropertyChanged
{
    private ObservableCollection<FileItem> _files;
    public ObservableCollection<FileItem> Files
    {
        get => _files;
        set
        {
            _files = value;
            OnPropertyChanged(nameof(Files));
            OnPropertyChanged(nameof(AllAttributes));
            OnPropertyChanged(nameof(ColumnConfig));
        }
    }
    
    public List<string> AllAttributes => GetUniqueAttributes();
    
    public object ColumnConfig => new AttributesToColumnsConverter().Convert(Files, null, null, null);
    
    // ... реализация INotifyPropertyChanged ...
}

Обработка отсутствующих атрибутов

Для отображения “none” вместо null используйте конвертер значений:

csharp
public class NoneValueConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null)
            return "none";
        return value;
    }
    
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Применение в XAML:

xml
<GridViewColumn DisplayMemberBinding="{Binding Attributes.[atr1], Converter={StaticResource NoneValueConverter}}"/>

Пример полного решения

MainWindow.xaml

xml
<Window x:Class="DynamicFilesViewer.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DynamicFilesViewer"
        Title="Dynamic Files Viewer" Height="500" Width="900">
    
    <Window.Resources>
        <local:AttributesToColumnsConverter x:Key="AttributesToColumnsConverter"/>
        <local:NoneValueConverter x:Key="NoneValueConverter"/>
    </Window.Resources>
    
    <Grid Margin="10">
        <ListView ItemsSource="{Binding Files}" 
                  HorizontalContentAlignment="Stretch"
                  Grid.Row="0">
            <ListView.View>
                <GridView>
                    <!-- Первая колонка - имя файла -->
                    <GridViewColumn Header="Файл" 
                                    DisplayMemberBinding="{Binding Name}" 
                                    Width="120"/>
                    
                    <!-- Остальные колонки создаются динамически -->
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>

MainWindow.xaml.cs

csharp
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new MainViewModel();
    }
}

public class MainViewModel : INotifyPropertyChanged
{
    public ObservableCollection<FileItem> Files { get; set; }
    
    public MainViewModel()
    {
        Files = new ObservableCollection<FileItem>();
        InitializeTestData();
        InitializeDynamicColumns();
    }
    
    private void InitializeTestData()
    {
        var file1 = new FileItem("Файл 1");
        file1.Attributes.atr1 = 1;
        file1.Attributes.atr3 = 4;
        Files.Add(file1);
        
        var file2 = new FileItem("Файл 2");
        file2.Attributes.atr2 = 5;
        file2.Attributes.atr4 = 19;
        Files.Add(file2);
        
        var file3 = new FileItem("Файл 3");
        file3.Attributes.atr1 = 7;
        file3.Attributes.atr = 4;
        Files.Add(file3);
    }
    
    private void InitializeDynamicColumns()
    {
        var converter = new AttributesToColumnsConverter();
        var gridView = (GridView)converter.Convert(Files, null, null, null);
        
        // Удаляем колонку с именем файла, так как она уже есть в XAML
        if (gridView.Columns.Count > 0)
        {
            gridView.Columns.RemoveAt(0);
        }
        
        // Добавляем динамические колонки
        foreach (var column in gridView.Columns)
        {
            // Применяем конвертер для отображения "none"
            if (column.DisplayMemberBinding is Binding binding)
            {
                binding.Converter = new NoneValueConverter();
            }
        }
        
        // Добавляем колонки в ListView
        var listView = Application.Current.MainWindow.FindName("listView") as ListView;
        if (listView?.View is GridView view)
        {
            foreach (var column in gridView.Columns)
            {
                view.Columns.Add(column);
            }
        }
    }
    
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Этот подход позволяет создавать полностью универсальный ListView, который自适应 под различные наборы атрибутов файлов и корректно отображает данные с обработкой отсутствующих значений.

Источники

  1. Dynamic generate column mvvm - Stack Overflow
  2. How to make a dynamic ListView in wpf? - Microsoft Learn
  3. Dynamic Grid View in WPF with MVVM pattern
  4. ListView with a GridView - The complete WPF tutorial
  5. WPF: Dynamic XAML - Microsoft Learn
  6. Using Dynamic Data Services in a WPF Application - CodeProject

Заключение

Для создания универсального ListView в WPF для отображения файлов с динамическими атрибутами необходимо:

  1. Выбрать правильную структуру данных - ExpandoObject или Dictionary<string, object> обеспечивают гибкость для хранения произвольных атрибутов
  2. Использовать ObservableCollection для автоматического обновления UI при изменении данных
  3. Реализовать конвертер для динамического создания GridView колонок на основе доступных атрибутов
  4. Обрабатывать отсутствующие значения через конвертеры для отображения “none”
  5. Следовать MVVM паттерну для разделения логики и представления

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

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