Как в WPF не вручную создать колонки для ListView?
Есть сотрудники, у них много полей - LastName, FirstName и т.д. Хочу отобразить список всех сотрудников в виде таблицы. Дело происходит в Wpf с MVVM паттерном. Так что у меня есть View с таким фрагментом:
<ListView
ItemsSource="{Binding Path=AllEmployees}"
>
<ListView.View>
<GridView>
<GridViewColumn
Header="Фамилия"
DisplayMemberBinding="{Binding Path=LastName}"
/>
<GridViewColumn
Header="Имя"
DisplayMemberBinding="{Binding Path=FirstName}"
/>
</GridView>
</ListView.View>
</ListView>
Соответственно, есть ViewModel, в которой AllEmployees - это коллекция с объектами сотрудников. Эти объекты как раз имеют свойства LastName, FirstName и т.д.
Что я хочу? Не объявлять каждый GridViewColumn руками и не задавать ему Header и биндинг, а объявить во ViewModel какой-нибудь словарь (или еще как-нибудь, я просто привел пример) вроде такого
public Dictionary<string, string> Columns = new Dictionary<string, string>()
{
{ "EmployeeId", "Табельный №" },
{ "TitleOfCourtesy", "Обращение" },
{ "LastName", "Фамилия" },
{ "FirstName", "Имя" },
{ "Title", "Должность" },
{ "HireDate", "Дата найма" },
{ "ReportsTo", "Подчиняется" }
};
и формировать GridViewColumn'ы из этого словаря. Соответственно, по ключу было бы понятно, какое свойство брать из объекта сотрудника, а по значению - какой заголовок давать для этого свойства в таблице.
Не могу понять, как это оформить через XAML. То ли через шаблон колумна как-то, то ли еще как.
Ответы (1 шт):
Подобное поведение можно реализовать при помощи AttachedProperty.
- Создаем статичный класс, назовем его допустим
GridViewColumns. В этом классе создаем три
AttachedProperty(в студии есть сниппет для этого -propa):- Первый назовем
Source(object) - будет служить для указания коллекции с колонками. - Второй будет
Header(string) - отвечать будет за заголовок. - Ну и последний,
DisplayMember(string) - отвечает за название свойства, которое данная колонка должна вывести.
Получаем в итоге нечто такое:
public static object GetSource(DependencyObject obj) => obj.GetValue(SourceProperty); public static void SetSource(DependencyObject obj, object value) => obj.SetValue(SourceProperty, value); public static readonly DependencyProperty SourceProperty = DependencyProperty.RegisterAttached("Source", typeof(object), typeof(GridViewColumns), new UIPropertyMetadata(default)); public static string GetHeader(DependencyObject obj) => (string)obj.GetValue(HeaderProperty); public static void SetHeader(DependencyObject obj, string value) => obj.SetValue(HeaderProperty, value); public static readonly DependencyProperty HeaderProperty = DependencyProperty.RegisterAttached("Header", typeof(string), typeof(GridViewColumns), new UIPropertyMetadata(default)); public static string GetDisplayMember(DependencyObject obj) => (string)obj.GetValue(DisplayMemberProperty); public static void SetDisplayMember(DependencyObject obj, string value) => obj.SetValue(DisplayMemberProperty, value); public static readonly DependencyProperty DisplayMemberProperty = DependencyProperty.RegisterAttached("DisplayMember", typeof(string), typeof(GridViewColumns), new UIPropertyMetadata(default));- Первый назовем
Теперь давайте подпишемся на обновление
Sourceи в обработчике этого события создадим тестовые колонки:Дописываем в регистрации у
Sourceнужный нам обработчикnew UIPropertyMetadata(default, SourceChanged)Создаем сам обработчик, в котором проверяем тип объекта и если есть новые объекты, то делаем колонки:
private static void SourceChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) { if (obj is GridView gridView) { if (e.NewValue != null) { ICollectionView view = CollectionViewSource.GetDefaultView(e.NewValue); if (view != null) { CreateColumns(gridView, view); } } } }Ну и простой метод генерации колонок:
private static void CreateColumns(GridView gridView, ICollectionView view) { foreach (var item in view) { GridViewColumn column = new GridViewColumn { Header = "Привет мир!" }; gridView.Columns.Add(column); } }
Привязываем все, запускаем и видим, что у нас создалась успешно тестовая колонка (в тестовой коллекции у меня 1 объект)
Отлично, пол дела сделано, остается нам поработать над заголовками и свойствами.
Создадим вспомогательный метод, который вернет значение свойства:
private static T GetPropertyValue<T>(object obj, string propertyName) { if (obj == null) return default; PropertyInfo prop = obj.GetType().GetProperty(propertyName); return (T)prop?.GetValue(obj, null); }Дальше для удобства вынесем генерацию одной колонки в отдельный метод.
В этом методе мы берем из свойствDisplayMemberиHeaderих значения и если они не пустые, то присваиваем их:private static GridViewColumn CreateColumn(GridView gridView, object columnSource) { GridViewColumn column = new GridViewColumn(); string header = GetHeader(gridView); string displayMemberMember = GetDisplayMember(gridView); if (!string.IsNullOrEmpty(header)) column.Header = GetPropertyValue<string>(columnSource, header); if (!string.IsNullOrEmpty(displayMemberMember)) { string propertyName = GetPropertyValue<string>(columnSource, displayMemberMember); column.DisplayMemberBinding = new Binding(propertyName); } return column; }Перепишем создание колонки в методе
CreateColumns:GridViewColumn column = CreateColumn(gridView, item);
Все, вот базовый пример подобной реализации, давайте испытаем:
Создаем тестовые данные:
class Person { public Person(string name, int age) => (Name, Age) = (name, age); public string Name { get; set; } public int Age { get; set; } } class Column { public Column(string header, string displayMember) => (Header, DisplayMember) = (header, displayMember); public string Header { get; } public string DisplayMember { get; } } class MainViewModel { public ObservableCollection<Column> Columns { get; } = new ObservableCollection<Column> { new Column("Имя", "Name") }; public ObservableCollection<Person> People { get; } = new ObservableCollection<Person> { new Person("Вася", 12), new Person("Петя", 19) }; }Привязываем все в XAML:
<ListView ItemsSource="{Binding People}"> <ListView.View> <GridView local:GridViewColumns.DisplayMember="DisplayMember" local:GridViewColumns.Header="Header" local:GridViewColumns.Source="{Binding Columns}" /> </ListView.View> </ListView>Здесь
DisplayMemberиHeader- имена соответствующих свойств из классаColumn, аSource- сама коллекция колонок.
И результатом будет одна колонка с именем человека:
Дополним еще пару колонок:
public ObservableCollection<Column> Columns { get; } = new ObservableCollection<Column>
{
new Column("Имя", "Name"),
new Column("Возраст", "Age"),
new Column("Неизвестная 1", null),
new Column("Неизвестная 2", null)
};
Результат:
Как видим, все успешно отработало. Я за основу взял этот ответ, в нем есть ряд вещей, которые я не стал переносить сюда, ибо моей задачей было показать базовый принцип, а дальнейшее развитие этого я оставляю на вас.


