将WPF DataGrid绑定到List <interface>

时间:2016-03-01 17:28:29

标签: c# wpf datagrid

你好,我有一个DataGrid,我想要显示不同的报告。我要改变课程,所以他们在这里更短,但是想法是一样的。

让我们说我有一个名为IReports的界面

public interface IReports
{
}

和三个叫做学生,班级,汽车的课程

public class Students:IReports
{
    public string Name { get; set; }
}  

public class Classes : IReports
{
    public string ClassName { get; set; }
    public string StudentName { get; set; }

}   
public class Cars : IReports
{
    public int Mileage { get; set; }
    public string CarType { get; set; }
    public string StudentName { get; set; }
}

列表

private List<IReports> _reportsTable;    

public List<IReports> ReportsTable
    {
        get { return _reportsTable; }
        set { SetProperty(ref (_reportsTable), value); }
    }

DataGrid

<DataGrid ItemsSource="{Binding ReportsList}"
          Grid.Column="1"
          Grid.Row="0"
          AutoGenerateColumns="True"
          Grid.RowSpan="6"/>

好的,所以重要的是它们都有不同的属性名称,而有些则有更少的属性名称。如何绑定DataGrid以查看不同的属性?如果这有任何不同,这就是MVVM。

更新:这将始终只使用其中一个类。但是当有人更改组合框时,它将触发一个将填充IList<IReports>的事件。

enter image description here

4 个答案:

答案 0 :(得分:3)

  

这将一次只使用其中一个类。但当有人更改组合框时,它会触发一个将填充IList&lt; IReports&gt;的事件。

我理解上述内容的方法是,您永远不要在列表中混合使用不同的元素(即它包含 ClassesStudentsCars)。所有其他答案都假设列表包含混合内容,但如果这是真的,则DataGrid根本不是此类内容的正确演示者。

如果上述假设是正确的,那么唯一的问题是如何用单个可绑定属性表示不同的列表。从Data Binding Overview可以看出,在处理集合时,数据绑定并不关心它们是否是通用的。可识别的源类型是非通用的IEnumerableIListIBindingList。但是,集合视图实现使用一些规则来确定集合的元素类型,方法是通过检查实际数据源类实现的IEnumerable<T>接口的泛型类型参数。第一个可用项目,或从ITypedList实施等获取信息。所有规则及其优先级都可以在Reference Source中看到。

考虑到所有这些,一种可能的解决方案是更改ReportsTable属性类型以允许分配List<Classes>List<StudentsList<Cars>。任何公共类/接口都可以工作(记住,数据绑定将检查GetType()返回的实际类型),例如objectIEnumerableIListIEnumerable<IReports>等。,所以我会选择与List<IReports IReadOnlyList<IReports>最近的协变类型:

private IReadOnlyList<IReports> _reportsTable;

public IReadOnlyList<IReports> ReportsTable
{
    get { return _reportsTable; }
    set { SetProperty(ref (_reportsTable), value); }
}

现在当你这样做

viewModel.ReportsTable = new List<Students>
{
    new Students { Name = "A" },
    new Students { Name = "B" },
    new Students { Name = "C" },
    new Students { Name = "D" },
};

你得到了

enter image description here

与此同时

viewModel.ReportsTable = new List<Classes>
{
    new Classes { ClassName = "A", StudentName = "A" },
    new Classes { ClassName = "A", StudentName ="B" },
    new Classes { ClassName = "B", StudentName = "C" },
    new Classes { ClassName = "B", StudentName = "D" },
};

显示

enter image description here

最后这个

viewModel.ReportsTable = new List<Cars>
{
    new Cars { Mileage = 100, CarType = "BMW", StudentName = "A" },
    new Cars { Mileage = 200, CarType = "BMW", StudentName = "B" },
    new Cars { Mileage = 300, CarType = "BMW", StudentName = "C" },
    new Cars { Mileage = 400, CarType = "BMW", StudentName = "D" },
};

结果

enter image description here

更新:以上内容需要修改模型以返回具体的List<T>实例。如果你想保持模型不变(即返回List<IReports>),那么你需要一个不同的解决方案,这次使用ITypedList。为此,我们将使用System.Collections.ObjectModel.Collection<T>基类创建一个简单的列表包装器:

public class ReportsList : Collection<IReports>, ITypedList
{
    public ReportsList(IList<IReports> source) : base(source) { }
    public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
    {
        return TypeDescriptor.GetProperties(Count > 0 ? this[0].GetType() : typeof(IReports));
    }
    public string GetListName(PropertyDescriptor[] listAccessors) { return null; }
}

然后将bindable属性更改为

private IList<IReports> _reportsTable;
public IList<IReports> ReportsTable
{
    get { return _reportsTable; }
    set { SetProperty(ref _reportsTable, value as ReportsList ?? new ReportsList(value)); }
}

你已经完成了。

答案 1 :(得分:1)

据我了解,您希望数据网格显示实现接口的各种类的各个列。如果挂钩DataGrid的LoadingRow事件,您可以看到在运行时处理的对象类型。您可以使用反射从行的datacontext中获取属性,然后检查datagrid以查看该属性是否有列。如果没有,请添加它。

如果列表中有不同的类型且类型没有其他类型的属性(例如,汽车没有名称属性且学生和汽车都在列表)。如果编辑对象上不存在的属性的列,则会引发异常。要解决这个问题,您需要一个将其应用于datagridcells的转换器和样式。为了好玩,我还添加了一个数据触发器,如果​​禁用它,则将单元格的背景更改为Silver。一个问题是如果您需要更改单元格的样式,那么您必须在代码中执行此操作(或者根据您的样式更改代码中的样式)。

XAML:

<DataGrid ItemsSource="{Binding ReportsTable}" AutoGenerateColumns="True" LoadingRow="DataGrid_LoadingRow" />

CS

private void DataGrid_LoadingRow(object sender, DataGridRowEventArgs e)
{
    var dg = sender as DataGrid;
    var pis = e.Row.DataContext.GetType().GetProperties();
    foreach (var pi in pis)
    {
        // Check if this property already has a column in the datagrid
        string name = pi.Name;
        var q = dg.Columns.Where(_ => _.SortMemberPath == name);
        if (!q.Any())
        {
            // No column matches, so add one
            DataGridTextColumn c = new DataGridTextColumn();
            c.Header = name;
            c.SortMemberPath = name;
            System.Windows.Data.Binding b = new Binding(name);
            c.Binding = b;

            // All columns don't apply to all items in the list
            // So, we need to disable the cells that aren't applicable
            // We'll use a converter on the IsEnabled property of the cell
            b = new Binding();
            b.Converter = new ReadOnlyConverter();
            b.ConverterParameter = name;

            // Can't apply it directly, so we have to make a style that applies it
            Style s = new Style(typeof(DataGridCell));
            s.Setters.Add(new Setter(DataGridCell.IsEnabledProperty, b));
            // Add a trigger to the style to color the background when disabled
            var dt = new DataTrigger() { Binding = b, Value = false };
            dt.Setters.Add(new Setter(DataGridCell.BackgroundProperty, Brushes.Silver));
            s.Triggers.Add(dt);
            c.CellStyle = s;

            // Add the column to the datagrid
            dg.Columns.Add(c);
        }
    }
}

转换器的CS:

public class ReadOnlyConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value != null)
        {
            var prop = value.GetType().GetProperty(parameter as string);
            if (prop != null)
                return true;
        }
        return false;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

而且,为了完成,这是我用于设置屏幕截图数据的内容:

public List<IReports> ReportsTable { get; set; }

public MainWindow()
{
    InitializeComponent();
    ReportsTable = new List<IReports>() {
        new Students() { Name = "Student 1" },
        new Students() { Name = "Student 2" },
        new Classes() { ClassName="CS 101", StudentName = "Student 3" },
        new Cars() { CarType = "Truck", Mileage=12345, StudentName = "Student 4" }
    };
    this.DataContext = this;
}

截图:

enter image description here

答案 2 :(得分:1)

为什么不在基本接口中添加一个getter,而不是显示给定字符串值的转换器选项。然后,每个类只返回它自己的,几乎就像每个对象都可以覆盖它的“ToString()”方法因为你要创建一个列表,例如用于显示或拾取,无论如何,该值都是只读的,使它只是一个吸气剂...

public interface IReports
{
    string ShowValue {get;}
}

public class Students:IReports
{
    public string Name { get; set; }
    public string ShowValue { get { return Name; } }
}  

public class Classes : IReports
{
    public string ClassName { get; set; }
    public string StudentName { get; set; }
    public string ShowValue { get { return ClassName + " - " + StudentName ; } }
}   
public class Cars : IReports
{
    public int Mileage { get; set; }
    public string CarType { get; set; }
    public string StudentName { get; set; }
    public string ShowValue { get { return CarType + "(" + Mileage + ") - " + StudentName; } }
}

然后在您的视图模型管理器中......

public class YourMVVMClass
{
        public YourMVVMClass()
        {
           SelectedRptRow = null;
           ReportsTable = new List<IReports>() 
           {
               new Students() { Name = "Student 1" },
               new Students() { Name = "Student 2" },
               new Classes() { ClassName="CS 101", StudentName = "Student 3" },
               new Cars() { CarType = "Truck", Mileage=12345, StudentName = "Student 4" }
           };
        }


        // This get/set for binding your data grid to
        public List<IReports> ReportsTable { get; set; }
        // This for the Selected Row the data grid binds to
        public IReports SelectedRptRow { get; set; }

        // This for a user double-clicking to select an entry from
        private void Control_OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
        {
            // Now, you can look directly at the SelectedRptRow
            // as in the data-grid binding declaration.
            if (SelectedRptRow is Classes)
                MessageBox.Show("User selected a class item");
            else if( SelectedRptRow is Cars)
                MessageBox.Show("User selected a car item");
            else if( SelectedRptRow is Students)
                MessageBox.Show("User selected a student item");
            else
                MessageBox.Show("No entry selected");
        }
}

最后在您的表单/视图中

    <DataGrid Grid.Row="0" Grid.Column="0"
        ItemsSource="{Binding ReportsTable}"
        SelectedItem="{Binding SelectedRptRow}" 
        MouseDoubleClick="Control_OnMouseDoubleClick"
        AutoGenerateColumns="False" 
        Width="200" Height ="140" 
        HorizontalAlignment="Left"
        VerticalAlignment="Top">

        <DataGrid.Columns>
            <DataGridTextColumn 
                Header="Report Item" 
                Width="180"
                IsReadOnly="True"
                CanUserSort="False"
                Binding="{Binding Path=ShowValue}" />
        </DataGrid.Columns>
    </DataGrid>

Result binding and double-click from datagrid

使用转换器的其他答案只是另一条路径,但这对我来说更容易,因为您可以根据需要更改每个单独的类并进行扩展/调整。暴露的“ShowValue”getter对于“IReports”的所有实例都是通用的,因此绑定是直接的,无需通过转换器。如果您删除一个类,或将来扩展,那么您的基础知识都是独立的。

现在不要误解我的意思,我确实使用转换器,通常使用布尔类型字段来分别显示,隐藏,折叠控件。这很好,因为我有不同的布尔转换器,如

BoolToVisibleHidden = if True, make visible vs Hidden
BoolToHiddenVisible = if True, make Hidden vs Visible
BoolToVisibleCollapse = if True, make visible vs Collapsed
BoolToCollapseVisible = if True, make Collapsed vs visible.

因此,在我的MVVM上有一个布尔属性,我可以显示和隐藏不同的控件......可能是管理员和标准用户选项。

我还使用转换器处理日期以进行替代格式化。

答案 3 :(得分:0)

您可以滥用IValueConverter。为每列创建一个 在ValueConverter中,您可以测试类型并返回正确的属性。我的意思的一个例子:

public class NameValueConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is Students)
        {
            return (value as Students).Name;
        }
        if (value is Classes)
        {
            return (value as Classes).ClassName;
        }
        if (value is Cars)
        {
            return (value as Cars).CarType;
        }
        return "";
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

要使用它,请将其作为资源添加到DataGrid:

<DataGrid.Resources>
    <local:NameValueConverter x:Key="NameValueConverter"></local:NameValueConverter>
</DataGrid.Resources>

并在绑定中指定它:

{Binding Path=., Converter={StaticResource NameValueConverter}}

此解决方案仅适用于只读DataGrids(编辑抛出NotImplementedException)。