简体   繁体   中英

WPF dynamic datagrid binding methods

I don't have a long experience of programming in c# and using WPF and I apologize if my "vocabulary" is not enough technical or appropriate.

I need to display a table in my application where rows are instances of a class and columns are properties/attributes that define my objects.

public class myObject
{
    //constructor
    public myObject() { }

    //properties
    public string Topic { get;  set}

    public string Description { get; set; }

    public bool Display { get; set; }

    public List<myAttribute> AttributeList { get; set; }


    //method
    public string GetAttributeValueByName(string sAttributeName)
    {
        foreach (myAttribute A in this.AttributeList)
        {
            if (A.Name == sAttributeName) { return A.Value.ToString(); }
        }

        return string.Empty;
    }
}

I have a list of myObjects that I bind as datagrid.ItemSource and I'm already able to properly bind defined properties like Topic/Description and display them in columns. myObject.Display is one-way binded as well and let me to visualize myObject in the datagrid or not (as a sort of filter which come from somewhere else).

Now, as you could notice in the code above, I have a property that return me a list of attributes. This because myObject have an undefined number of free attributes which the user can set . I have set a proper class for this Attributes (with few simple properties like .Name , .Value , .Color , .Rank ). I have methods that let me to retrieve the value as a string for example or so.

My main problem is that I'm not able to visualize them properly in the datagrid because, in general, the binding works with properties and not with methods. I did some resarch about binding a method and I've tried with any successful result.

Briefly:

  • I need dynamic columns (some fixed like Topic + one for each attribute)
  • all myObjects have same number of element in AttributeList
  • I need to bind (at least one-way) the value of an attribute in the proper cell of the datagrid.

UI class:

namespace myApp.UserInterface
{
    public partial class control : UserControl
    {
        control()
        {
            InitializeComponent();
            InitializeData();
        }

        private InitializeData()
        {
            datagrid.ItemsSource = Core.myObjectPoolList;
        }
    }
}

UI XAML

<UserControl x:Class="myApp.UserInterface.control"
         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:myApp.UserInterface;assembly="
         xmlns:core="clr-namespace:myObject.Core;assembly="
         xmlns:system="clr-namespace:System;assembly=mscorlib"
         mc:Ignorable="d" 
         d:DesignHeight="300" d:DesignWidth="300">
    <UserControl.Resources>
        <ObjectDataProvider x:Key="basic_attribute_value"
                            ObjectType="{x:Type core:D2P_Issue}"
                            MethodName="GetAttributeValueByName"
                            IsAsynchronous="True">
            <ObjectDataProvider.MethodParameters>
                <system:String>status</system:String>
            </ObjectDataProvider.MethodParameters>
        </ObjectDataProvider>
    </UserControl.Resources>

    <Grid>
        <DataGrid Name="issuepool_datagrid" AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Topic" Binding="{Binding Topic}" Width="150" TextBlock.TextAlignment="Center"/>
                <DataGridTextColumn Header="Description" Binding="{Binding Description}" Width="150" TextBlock.TextAlignment="Center"/>
                <DataGridTextColumn x:Name="status_column" Header="status" Binding="{Binding Source={StaticResource basic_attribute_value}, Mode=OneWay}" Width="150" TextBlock.TextAlignment="Center"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</UserControl>

In this example I was trying to leave behind the dynamic generation of columns and just hardcode it to look for the "status" attribute name.

Feel free to ask if something is not clear.

You could create the columns dynamically in code behind. Pretty simple though. If you were going to reuse this multiple times in the same project, you could rewrite this as an attached behavior, and store the collection of dynamic columns on the grid itself using another attached property.

It's up to you to figure out when the set of attributes changes, and update the columns accordingly. I've stored the dynamic columns in a dictionary to simplify the business of updating them intelligently, if you happen to know that only one attribute was added or removed.

If were you, I'd replace myObject.AttributeList with Dictionary<String, myAttribute> as well. The binding path would have to change slightly.

This allows the user to edit the attribute values. You could set IsReadOnly = true on the dynamic columns if you like.

public partial class control : UserControl
{
    control()
    {
        InitializeComponent();
        InitializeData();
    }

    private InitializeData()
    {
        datagrid.ItemsSource = Core.myObjectPoolList;

        var firstItem = Core.myObjectPoolList.First();

        if (firstItem != null) {
            UpdateDataGridColumns(dataGrid, firstItem.AttributeList, _dynamicColumns);
        }
    }

    private Dictionary<string, DataGridColumn> _dynamicColumns = new Dictionary<string, DataGridColumn>();
    protected void UpdateDataGridColumns(DataGrid dg, List<myAttribute> attributesSample, Dictionary<string, DataGridColumn> existingDynamicColumns)
    {
        foreach (var col in existingDynamicColumns.Values)
        {
            dg.Columns.Remove(col);
        }
        existingDynamicColumns.Clear();

        int idx = 0;
        foreach (var attr in attributesSample)
        {
            var column = new DataGridTextColumn() {
                Header = attr.Name,
                Binding = new Binding($"AttributeList[{idx}].Value")
            };

            dg.Columns.Add(column);

            existingDynamicColumns.Add(attr.Name, column);

            ++idx;
        }
    }
}

Alternate approach: Put all the attributes in one column. This doesn't use the code behind above. This is readonly in this form, but could be adapted to let the user edit the values in the grid.

Add this property to myObject :

    public IEnumerable<String> AttributeNames 
        => AttributeList.Select(a => a.Name);

And this to DataGrid.Columns in the XAML:

<DataGridTemplateColumn
    Width="*"
    >
    <DataGridTemplateColumn.HeaderStyle>
        <Style>
            <Setter Property="ContentControl.HorizontalContentAlignment" Value="Stretch" />
        </Style>
    </DataGridTemplateColumn.HeaderStyle>
    <DataGridTemplateColumn.HeaderTemplate>
        <DataTemplate>
            <ItemsControl
                ItemsSource="{Binding DataContext.Items[0].AttributeNames, RelativeSource={RelativeSource AncestorType=DataGrid}}"
                >
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <UniformGrid />
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Label 
                            Content="{Binding}" 
                            />
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </DataTemplate>
    </DataGridTemplateColumn.HeaderTemplate>
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <ItemsControl
                ItemsSource="{Binding AttributeList}"
                >
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <UniformGrid />
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Label Content="{Binding Value}" />
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

Why not use a DataGridComboBoxColumn ?

Something like this:

<DataGridComboBoxColumnHeader="Attributes" Binding="{Binding AttributeList}" width="150" TextBlock.TextAlignment="Center"/>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM