【问题标题】:How to autogenerate DataGrid columns for collection of child type from collection of base type in WPF?如何从 WPF 中的基类型集合中为子类型集合自动生成 DataGrid 列?
【发布时间】:2020-11-04 20:19:44
【问题描述】:

所以我有一堆派生自某个基类的类。我有一个类(收集器),它们有一个返回这些类的集合的方法。我还有一个TabControl,其中每个选项卡都有包含DataGrid 的自定义控件。这些自定义控件有一个ViewModel。在 ViewModel 中,我收集了收集器返回的基类元素。我想将DataGrids 绑定到这些集合并自动生成列,但是派生类具有不同的属性,而基类没有任何应该显示的属性。

internal class ElementsInfoViewModel : INotifyPropertyChanged
{
    private readonly ElementScaner _elementScaner;

    public ElementsInfoViewModel(ElementScaner elementScaner)
    {
        _elementScaner = elementScaner;
    }

    public ReadOnlyCollection<SystemElement> ShownElements => _elementScaner.Elements;
}

<UserControl x:Class="SysSpy.Desktop.Controls.ElementsInfoTabItemContent"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:toolkit="http://schemas.microsoft.com/wpf/2008/toolkit"
             xmlns:local="clr-namespace:SysSpy.Desktop.Controls"
             mc:Ignorable="d">
    <Grid>
        <toolkit:DataGrid ItemsSource="{Binding ShownElements}">
        </toolkit:DataGrid>
    </Grid>
</UserControl>

我从代码中删除了所有不必要的内容。

我想到的想法是以某种方式将基类型的集合转换为派生类型的集合(可能使用 IValueConverter),但是随着每秒更新多次集合,反射可能是一个不好的解决方案

UPD

TabControl 绑定到主虚拟机中的某个集合

 internal class MainViewModel : INotifyPropertyChanged
    {
        public MainViewModel()
        {
            Test = new ObservableCollection<ElementsInfoViewModel>();

            var certificatesCollector = new CertificatesCollector();
            var certificatesScaner = new ElementScaner(certificatesCollector, "Certificates");
            certificatesScaner.Scan();
            var certifVM = new ElementsInfoViewModel(certificatesScaner);

            Test.Add(certifVM);
        }

        public ObservableCollection<ElementsInfoViewModel> Test { get; set; 
}

    }

<Window x:Class="SysSpy.Desktop.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:SysSpy.Desktop"
        xmlns:viewModel="clr-namespace:SysSpy.Desktop.ViewModels"
        xmlns:controls="clr-namespace:SysSpy.Desktop.Controls"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    <Window.Resources>
        <viewModel:MainViewModel x:Key="viewModelSource"/>
    </Window.Resources>

    <Window.DataContext>
        <Binding Source="{StaticResource viewModelSource}"/>
    </Window.DataContext>

    <DockPanel LastChildFill="True">
        <Grid>
            <TabControl ItemsSource="{Binding Test}">
                <TabControl.ItemTemplate>
                    <DataTemplate DataType="viewModel:ElementsInfoViewModel">
                        <controls:ElementsInfoTabItemHeader/>
                        
                    </DataTemplate>
                </TabControl.ItemTemplate>
                <TabControl.ContentTemplate>
                    <DataTemplate DataType="viewModel:ElementsInfoViewModel">
                        <controls:ElementsInfoTabItemContent/>
                        
                    </DataTemplate>
                </TabControl.ContentTemplate>
                
            </TabControl>
            
        </Grid>
    </DockPanel>
</Window>

【问题讨论】:

    标签: c# wpf mvvm datagrid


    【解决方案1】:

    我不知道这是否是您需要的,但我有另一个选择。 由于 DataGrid 没有动态绑定列的方法,因此您可以扩展它并自行制作。

    例如这样:

    class MyDataGrid : DataGrid
    {
    
        public MyDataGrid() : base()
        {
        }
    
        protected override void OnItemsSourceChanged(System.Collections.IEnumerable oldValue, System.Collections.IEnumerable newValue)
        {
            base.OnItemsSourceChanged(oldValue, newValue);
            if (newValue != null)
            {
                var enumerator = newValue.GetEnumerator();
                if (enumerator.MoveNext())
                {
                    Columns.Clear();
                    var firstElement = enumerator.Current;
                    var actualType = firstElement.GetType();
                    foreach (var prop in actualType.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(x => x.CanRead))
                    {
                        Columns.Add(new DataGridTextColumn
                        {
                            Header = prop.Name,
                            Binding = new Binding(prop.Name)
                        });
                    }
    
                }
            }
        }
    }
    

    我写了一个简单的项目来测试它,它可以在我的环境中运行。

    第一个对象类型(人)

    class Person 
    {
    
        public string Name { get; set; }
    
        public string Surname { get; set; }
    
        public int Age { get; set; }
    }
    

    第二个对象类型(汽车)

    class Car 
    {
    
        public string Name { get; set; }
    
    
        public int HP { get; set; }
    }
    

    你称为 SystemElement 的元素(标题用作标签页眉)

    class SystemElement
    {
    
        public SystemElement(string title, IList<object> elements)
        {
            Title = title;
            Elements = new ReadOnlyCollection<object>(elements);
        }
    
    
        public string Title { get; set; }
    
    
        public ReadOnlyCollection<object> Elements { get; }
    
    
    }
    

    ElementScanner 提取要显示的元素。在我的测试中,列表是在构造函数中设置的:

    class ElementScanner
    {
    
        public ElementScanner()
        {
            var data = new List<SystemElement>();
    
            data.Add(new SystemElement("People", new List<object>
            {
                new Person { Name = "John", Surname = "Doe", Age = 22},
                new Person { Name = "Lenny", Surname = "Pegasus", Age = 30},
                new Person { Name = "Duffy", Surname = "Duck", Age = 22}
            }));
    
            data.Add(new SystemElement("Cars", new List<object>
            {
                new Car { Name = "Mercedes", HP = 700 },
                new Car { Name = "Red Bull", HP = 650 },
                new Car { Name = "Ferrari", HP = 600 }
            }));
    
            Elements = new ReadOnlyCollection<SystemElement>(data);
        }
    
        public ReadOnlyCollection<SystemElement> Elements { get; }
    }
    

    现在我们有了 ViewModel。在我的示例中,我没有使用 INotifyPropertyChanged,因为属性永远不会改变(这是一个测试项目)。

    class ElementsInfoViewModel
    {
    
        public ElementsInfoViewModel()
        {
            var elementScanner = new ElementScanner();
            Elements = elementScanner.Elements;
        }
    
        public ReadOnlyCollection<SystemElement> Elements { get; }
    }
    

    现在移动到视图侧。首先,我们有一个表示每个选项卡内容的 UserControl。所以它会显示一个 MyDataGrid 类型的 DataGrid:

    <UserControl x:Class="Stack.ElementInfoView"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
                 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
                 xmlns:local="clr-namespace:Stack"
                 mc:Ignorable="d" 
                 d:DesignHeight="450" d:DesignWidth="800">
        <Grid>
            <local:MyDataGrid ItemsSource="{Binding Elements}" >
            </local:MyDataGrid>
        </Grid>
    </UserControl>
    

    最后我们有了将所有的主视图合并到一起:

    <Window x:Class="Stack.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:Stack"
            mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800">
        <Window.DataContext>
            <local:ElementsInfoViewModel />
        </Window.DataContext>
        <Grid>
            <TabControl ItemsSource="{Binding Elements}">
                <TabControl.ItemContainerStyle>
                    <Style TargetType="TabItem">
                        <Setter Property="Header" Value="{Binding Title}" />
                    </Style>
                </TabControl.ItemContainerStyle>
                <TabControl.ContentTemplate>
                    <DataTemplate DataType="local:ElementsInfoViewModel">
                        <local:ElementInfoView></local:ElementInfoView>
                    </DataTemplate>
                </TabControl.ContentTemplate>
            </TabControl>
        </Grid>
    </Window>
    

    这就是我们所拥有的(我想这就是你想要的):

    【讨论】:

    • 嗯...听起来不错。尝试后会回复您。最重要的是,我可以为一些我不想显示的属性添加属性
    • 确实!您可以使用数据属性管理任何自定义(例如,您可以使用组合框列而不是文本)
    【解决方案2】:

    我不知道它是否有效,但您是否尝试过使用泛型? 我的意思是这样的:

    internal class ElementsInfoViewModel<T> : INotifyPropertyChanged
                   where T : SystemElement
    {
    
        public ElementsInfoViewModel(ElementScaner elementScaner)
        {
            var list = new List<T>();
            foreach (var el in elementScanner.Elements)
            {
               list.Add(el as T);
            }
            ShownElements = new ReadOnlyCollection<T>(list);
        }
    
        public ReadOnlyCollection<T> ShownElements { get; }
    }
    

    我在浏览器中编写它,所以我的代码可能是错误的,但请接受这个想法。 如果可以的话,最好是拥有 ElementScanner 的通用版本(类似于 ElementScanner )。

    让我们知道它是否有效

    【讨论】:

    • 首先我想使用泛型,但这不适用于我的情况,因为我有一个 ElementsInfoViewModel 的集合,所以我不能有 List&lt;ElementsInfoViewModel&lt;&gt;&gt;
    猜你喜欢
    • 1970-01-01
    • 2019-12-05
    • 2011-02-25
    • 1970-01-01
    • 1970-01-01
    • 2021-11-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多