4

I have been trying to make a an editable DataGrid with dynamic columns in a WPF MVVM project. The dynamic columns would be the same type, i.e: decimal.

The aim is to collect department totals of shops with indefinite number of departments. I tried to demonstrate it below.

Day Dept1   Dept2   Dept3... TotalOfDepartments CashTotal CreditTotal
=====================================================================
1    100     200     50            350             50       300
2     75     100      0            175             25       150  

So, there are numerous shops with indefinite departments and my goal is to collect month

I want to make Department, CashTotal & CreditTotal Columns editable. I've had several approaches that I tried like:

This is my last try from the last approach. As follows:

Model:

 public class DailyRevenues
    {
        public int ShopId { get; set; }
        public int Day { get; set; }
        public ObservableCollection<Department> DepartmentList { get; set; }

        public DailyRevenues()
        {
            this.DepartmentList = new ObservableCollection<Department>();
        }
    }

    public class Department
    {
        public string Name { get; set; }

        private decimal total;
        public decimal Total
        {
            get { return total; }
            set { total = value; }
        }
    }

ViewModel:

public class DataItemViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public DataItemViewModel()
        {
            this.MonthlyRevenues = new ObservableCollection<DailyRevenues>();

            var d1 = new DailyRevenues() { ShopId = 1, Day = 1 };
            d1.DepartmentList.Add(new Department() { Name = "Deapartment1", Total = 100 });
            d1.DepartmentList.Add(new Department() { Name = "Deapartment2", Total = 200 });

            var d2 = new DailyRevenues() { ShopId = 1, Day = 2 };
            d2.DepartmentList.Add(new Department() { Name = "Deapartment1", Total = 75 });
            d2.DepartmentList.Add(new Department() { Name = "Deapartment2", Total = 150 });
            d2.DepartmentList.Add(new Department() { Name = "Deapartment3", Total = 100 });

            this.MonthlyRevenues.Add(d1);
            this.MonthlyRevenues.Add(d2);
        }

        private ObservableCollection<DailyRevenues> monthlyRevenues;
        public ObservableCollection<DailyRevenues> MonthlyRevenues
        {
            get { return monthlyRevenues; }
            set
            {
                if (monthlyRevenues != value)
                {
                    monthlyRevenues = value;
                    OnPropertyChanged(nameof(MonthlyRevenues));
                }
            }
        }

        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

And the XAML:

<DataGrid ItemsSource="{Binding MonthlyRevenues}" AutoGenerateColumns="False" >
        <DataGrid.Columns>
            <DataGridTextColumn Header="Day" Binding="{Binding Path=Day}" />
            <DataGridTextColumn Header="{Binding Path=MonthlyRevenues[0].DepartmentList[0].Name}" Binding="{Binding Path=DepartmentList[0].Total, Mode=TwoWay}" />
            <DataGridTextColumn Header="{Binding Path=DepartmentList[1].Name}" Binding="{Binding Path=DepartmentList[1].Total, Mode=TwoWay}" />
            <DataGridTextColumn Header="Department Total"/>
            <DataGridTextColumn Header="Cash Total" />
            <DataGridTextColumn Header="Credit Total" />
        </DataGrid.Columns>
    </DataGrid>

Unfortunately, on this last try using the indexers on XAML does not help me on dynamic columns and I can not find a way to bind them any other way.

More info: The datagrid (and the data demonstration) above belongs to shop1 and I want to collect monthly revenues of it`s departments on a window/user control . Each shop has the same count of departments throughout the month, but this does not mean that every department should have revenues each day, it can be zero. The department may be closed for any day, so does not raise any revenue for the day. Shop2 might have entirely different departments for the same month, so I will not handle all shops in the same screen.

EDIT 1: More info about scenario added.

G.Anıl Yalçın
  • 188
  • 1
  • 14
  • have you seen/considered binding a DataTable? example: https://stackoverflow.com/a/44206066/1506454 – ASh Jul 06 '17 at 20:21
  • @ASh, How can I make it two-way. I mean editing is OK, but how do I get the data on viewmodel to do some magic back there? – G.Anıl Yalçın Jul 06 '17 at 20:24
  • 1
    what should be two-way? with a DataTable binding click on DataGrid cell should allow to edit value. the cahnge will be reflected in dataTable cell – ASh Jul 06 '17 at 20:27
  • DataTable binding click on DataGrid cell with mvvm? I shall look into that. – G.Anıl Yalçın Jul 06 '17 at 20:32
  • 1
    DataTable is not going to help you with the variable number of columns. You are asking a lot out of a row/column structure. There is a way to do what you are asking, but I need to understand exactly what you are doing. In your example, on day 1 shop 1 has 2 depts. On day 2 it has 3, 2 of which have the same names as day 1. What if all the names on day 2 are different? Are there then 5 columns? If shop 2 has depts. of the same name are they the same column? And so on. – AQuirky Jul 06 '17 at 22:33

1 Answers1

3

There are a number of different approaches you could take, each with pluses and minuses. Based on your more complete description of the problem, I have chosen the custom type descriptor approach.

Here we add a custom type descriptor to the daily revenues class...

public class DailyRevenues : ICustomTypeDescriptor
{
    public int ShopId { get; set; }
    public int Day { get; set; }
    public ObservableCollection<Department> DepartmentList { get; set; }

    public DailyRevenues()
    {
        this.DepartmentList = new ObservableCollection<Department>();
    }
    public decimal TotalOfDepartments { get;  }
    public decimal CashTotal { get;  }
    public decimal CreditTotal { get; }

    public AttributeCollection GetAttributes()
    {
        return new AttributeCollection();
    }

    public string GetClassName()
    {
        return "DailyRevenues";
    }

    public string GetComponentName()
    {
        return "";
    }

    public TypeConverter GetConverter()
    {
        return null;
    }

    public EventDescriptor GetDefaultEvent()
    {
        return null;
    }

    public PropertyDescriptor GetDefaultProperty()
    {
        return null;
    }

    public object GetEditor(Type editorBaseType)
    {
        return null;
    }

    public EventDescriptorCollection GetEvents()
    {
        return null;
    }

    public EventDescriptorCollection GetEvents(Attribute[] attributes)
    {
        return null;
    }

    public PropertyDescriptorCollection GetProperties()
    {
        PropertyDescriptorCollection pdc0 = TypeDescriptor.GetProperties(typeof(DailyRevenues));
        List<PropertyDescriptor> pdList = new List<PropertyDescriptor>();
        pdList.Add(pdc0["Day"]);
        for (int i = 0; i < DepartmentList.Count; ++i)
        {
            pdList.Add(new DailyRevenuesProperty(DepartmentList[i].Name, i));
        }
        pdList.Add(pdc0["TotalOfDepartments"]);
        pdList.Add(pdc0["CashTotal"]);
        pdList.Add(pdc0["CreditTotal"]);
        return new PropertyDescriptorCollection(pdList.ToArray());
    }

    public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
    {
        return GetProperties();
    }

    public object GetPropertyOwner(PropertyDescriptor pd)
    {
        return this;
    }
}

The custom type descriptor allows us to "flatten" the data structure. As the number of departments change, the number of properties on the object changes. This requires a custom property descriptor for the daily revenues class...

public class DailyRevenuesProperty : PropertyDescriptor
{
    int _index;
    public DailyRevenuesProperty(string name, int index)
        : base(name, new Attribute[0])
    {
        _index = index;
    }
    public override Type ComponentType
    {
        get
        {
            return typeof(DailyRevenues);
        }
    }

    public override bool IsReadOnly
    {
        get
        {
            return false;
        }
    }

    public override Type PropertyType
    {
        get
        {
            return typeof(decimal);
        }
    }

    public override bool CanResetValue(object component)
    {
        return false;
    }

    public override object GetValue(object component)
    {
        DailyRevenues dr = component as DailyRevenues;
        if(dr != null && _index >= 0 && _index < dr.DepartmentList.Count)
        {
            return dr.DepartmentList[_index].Total;
        }
        else
        {
            return (decimal)0;
        }
    }

    public override void ResetValue(object component)
    {
    }

    public override void SetValue(object component, object value)
    {
        DailyRevenues dr = component as DailyRevenues;
        if (dr != null && _index >= 0 && _index < dr.DepartmentList.Count && value is decimal)
        {
            dr.DepartmentList[_index].Total = (decimal)value;
        }
    }

    public override bool ShouldSerializeValue(object component)
    {
        return false;
    }
}

Now we need a typed list. This replaces the observable collection.

public class MonthlyRevenues : ObservableCollection<DailyRevenues>, ITypedList
{
    public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
    {
        if(Count > 0)
        {
            return TypeDescriptor.GetProperties(this[0]);
        }
        else
        {
            return TypeDescriptor.GetProperties(typeof(DailyRevenues));
        }
    }

    public string GetListName(PropertyDescriptor[] listAccessors)
    {
        return "Monthly Revenues";
    }
}

When auto generating columns the data grid checks to see if the items collection is a typed list. If it is, the data grid queries for the properties on the typed list.

Finally to wrap things up, here is the data grid...

    <DataGrid ItemsSource="{Binding MonthlyRevenues}" AutoGenerateColumns="true" />

And this is the resulting grid...

enter image description here

There are number of limitations to this approach. First I am relying on the data grid to autogenerate the columns. If I want to add things like spaces to the header text, I will need to do some more stuff. Second I am counting on the department names to be valid property names and to not collide with other properties in the daily revenues class. If not, then I will need to do some more stuff. And so on.

AQuirky
  • 4,691
  • 2
  • 32
  • 51
  • I appreciate your help. I have studied your approach and I think I somehow still can not explain my problem the way I should. I have made changes in the view model according to your changes by declaring a `MonthlyRevenues` property instead of `ObservableCollection`. I know datagrid editability of `DailyRevenues : ICustomTypeDescriptor` class' properties depends on setters. `DepartmentList` has a setter but the resulting grid is not editable on columns which Department 1 & 2. Do I need another _typed list_ or another _Custom Type Descriptor_? – G.Anıl Yalçın Jul 09 '17 at 14:55
  • 1
    No. This is easily fixed. I didn't realize you wanted department totals editable because there was no setter on it. I've edited the answer so you can now edit department totals. The changes (besides adding setter to department total) are to the property descriptor: (1) change IsReadOnly to false and (2) implement the SetValue function. – AQuirky Jul 09 '17 at 15:36
  • This looks promising. But there is still something I can not figure out. How can I fire propertyChanged on changes of Department Total, i.e change Department1 from 100 to 200, so that I can calculate TotalOfDepartments again? – G.Anıl Yalçın Jul 09 '17 at 17:05
  • 1
    Very straightforward. Implement the INotifyPropertyChanged interface on Department and listen for changes to the Total property in DailyRevenues. – AQuirky Jul 09 '17 at 17:11
  • Thanks, AQuirky took some time but your answer led me to a solution. I will edit the question and post my approach soon. – G.Anıl Yalçın Jul 25 '17 at 20:07