Hello,
I have two problems with selfrefencing hierarchy in GridView. First, my test application is related to this thread, just for info and context, but I am asking question in new thread, as it was recommended to me, and I think this is separate problem not related to original design question.
https://www.telerik.com/forums/gridview---design-of-parameter-editor-with-subitems
I have created more realistic test application (TelerikTestReal in attached project) with new data structures for self referencing hierarchy binding. I can see desired output after program startup. When I add new main item programatically, GridView shows it properly. But, when I change test type of main item and subitems are changed, deleted child rows disappear, but newly added are not shown, as you can see on animation below. After some debugging, I see child rows are added to BindingList bound to DataSource of GridView, I also see ListChanged event of BindingList is properly fired, but GridView doesn't show new child rows. I can show them by calling ResetBindings() method of BindingList. What can be the problem?
The second problem is that I cannot close program after call to ResetBindings() method, I can click close button manytimes, but nothing happens.
3 Answers, 1 is accepted
Hello, Marian,
Thank you for providing a test application and submitting a new thread that describes a different problem than the previous one.
When investigating the provided project, I noticed that you have BindingList<TestParEditorItem> for the data source but you use Linq queries for the GridViewComboBoxColumn that stores the data, and indeed you call.ToList method in the end of statement. The following code is extracted from your MainForm file:
col.DataSource = ((TestType[])Enum.GetValues(typeof(TestType))).Select(v => new { Value = v, Display = v.ToString() }).ToList();
ToList method makes the collection to non IBinding collection. Although List is a generic collection and it is convenient for storing a number of business objects, it does not support the two-way binding mechanism needed for the purposes of the notifications. This is why RadGridView is not synchronized, simply because it is not notified about the change in the collection.
I highly recommend you to refer to the following article where different cases about reflecting changes in RadGridView are described: Reflecting Custom Object Changes in RGV - RadGridView - Telerik UI for WinForms
In your case, it is necessary to call the ResetBindings() method as you have already found out, in order to notify RadGridView that the TestType in the combo column has changes and update it accordingly by adding desired sub items. The right place of this to happen in directly into the TestParEditorCollection.cs in Items_ListChanged event which you handle. I have updated it in the following way and the grid now shows correct:
private void Items_ListChanged(object sender, ListChangedEventArgs e)
{
if ((e.ListChangedType == ListChangedType.ItemChanged) && (e.PropertyDescriptor.Name == "TestType"))
{
HandleTestTypeChanged(Items[e.NewIndex]);
Items.ResetBindings();
}
}
Also, I am able to edit cells further, and close the form. See the attached gif file.
I hope this helps. If you have any other questions do not hesitate to contact me.
Regards,
Nadya | Tech Support Engineer
Progress Telerik
Hello,
thanks for answer. First, about that LINQ for DataSource of combo box column. Ok, so what's the right way to have enum values in combo box? I have looked to documentation, all samples are using DataTable, I think it's obsolete now. That LINQ with anonymous classes was copied from WinForms DataGridView object.
But, I don't agree the problem is in this. When I debug this code, I put breakpoint to Items_ListChanged of my collection, I can see changed TestType, HandleTestTypeChanged method is properly called, it adds two new subitems to BindingList with correct GridID / GridParentID. Next, I see two other calls to Items_ListChanged with ListChangedType == ListChangedType.ItemAdded, and I can see BindingList notification of two new subitems.
private void Items_ListChanged(object sender, ListChangedEventArgs e)
{
if ((e.ListChangedType == ListChangedType.ItemChanged) && (e.PropertyDescriptor.Name == "TestType"))
{
HandleTestTypeChanged(Items[e.NewIndex]);
}
if(e.ListChangedType == ListChangedType.ItemAdded)
{
string subName = Items[e.NewIndex].SubItemName;
// OK, fired
}
}
Also, when I change som other item with subitems and change type to other without subitems, I can see subitems are deleted:
So I think GridView was notified about changes, but it has some problem with newly added subitems. Btw. I can add subitems also programmatically without changing main item, so I don't think problem will be in this. Or am I missing something?
Hello,
I forgot to comment the second problem with freezing yesterday. Today I have figured out the problem is not in changing test type or in adding child items, but generally when I call ResetBindings while GridView is edit state. See this animation. Can you please try also this?
Hello, Marian,
The way you bind the GridViewComboBox is correct, the data you want is displayed correctly in grid. However, in order to update the grid about the changes it should be notified. This is why the ResetBindings should be used. More information is availabale here: Reflect Data Source Updates in Control with BindingSource - Windows Forms .NET Framework | Microsoft Learn
From the attached gif file I can see that you modified "TestZatky" to become "RelVyska". "RelVyska" item does not have sub items. Hence, it is expected that no sub items with appear if you choose an item without subitems. I am a little confused. Can you please clarify what exactly you mean with "when I change some other item with subitems and change type to other without subitems, I can see subitems are deleted"? Am I missing something here?
As to the other problem with freezing, after calling the ResetBindings and the grid is updated, the form closes when I am in edit mode. See the following gif file:
Let me know if I can assist you further.
Regards,
Nadya | Tech Support Engineer
Progress Telerik
Love the Telerik and Kendo UI products and believe more people should try them? Invite a fellow developer to become a Progress customer and each of you can get a $50 Amazon gift voucher.
Hello,
maybe I should explain it more. Subitems for test items are dependent on selected test type. Most test types has no subitems, but there are three special tests (TestNitu, TestZatky and TestKolika) which has more results (subitems), TestNitu has 4 subitems, TestZatky and TestKolika have 2 subitems. So when test type for item is changed and new test type has different requested subitem count, subitems are internally modified in TestParItem.TestType property. More precisely, GridView is bound to collection of TestParEditorItem, which passes TestType property to underlying TestParItem and then calls NotifyPropertyChanged(). So when NotifyPropertyChanged() is called here, there are already new subitems generated in underlying TestParItem. You can see it here:
public TestType? TestType
{
get => IsItem ? (TestType?)Item.TestType : null;
set
{
if (IsItem)
{
if (Item.TestType != value.Value)
{
Item.TestType = value.Value;
NotifyPropertyChanged();
}
}
}
}
Next, TestParEditorCollection is notified about changed test type in Items_ListChanged, this is handled here (I have added prints to console, so I can see events in Output window):
private void Items_ListChanged(object sender, ListChangedEventArgs e)
{
Console.WriteLine($"ListChanged, Type={e.ListChangedType}, NewIndex={e.NewIndex}, PropertyDescr={(e.PropertyDescriptor?.Name ?? "--")}");
if ((e.ListChangedType == ListChangedType.ItemChanged) && (e.PropertyDescriptor != null) && (e.PropertyDescriptor.Name == "TestType"))
{
Console.WriteLine($"TestType changed, GridID={EditorItems[e.NewIndex].GridID}, value={EditorItems[e.NewIndex].TestType}");
HandleTestTypeChanged(EditorItems[e.NewIndex]);
//EditorItems.ResetBindings();
}
if (e.ListChangedType == ListChangedType.ItemAdded)
{
Console.WriteLine($"ItemAdded, GridID={EditorItems[e.NewIndex].GridID}, ParentID={EditorItems[e.NewIndex].GridParentID}");
string subName = EditorItems[e.NewIndex].SubItemName;
// OK, fired
}
}
Items_ListChanged event handler is notified about TestType property is changed, e.ListChangedType is ItemChanged, e.PropertyDescriptor.Name is TestType, so handler knows that subitems were changed, so it calls HandleTestTypeChanged() method, which removes old subitems first and then generates new subitems and adds them to collection. They are added and BindingList notifies GridView it contains new items, Items_ListChanged event handler is again called for each new subitems with e.ListChangedType = ItemAdded. I handled also this case and report it to console. You can see it in animation below. You can see 3 event calls there, first about changed test type, you can see there correct GridID and new TestType value, and then you can see 2 events about added subitems, also, you can see correct GridParentID and new GridIDs for subitems. So I think GridView should be correctly notified about all this, but it doesn't show new subitems.
I am also confused, what's going on there, it's strange. I think all notifications should be ok, I don't know why GridView doesn't show new subitems. If I do opposite change of TestType, when I change TestType from TestZatky to something else without subitems, you can see old subitems are removed, you can see it on my animation from previous comment, where P8 is changed from TestZatky to RelVyska and you see 2 subitems (Hlbka + Naklon) are removed. But generally problem is that GridView doesn't show newly added subitems, you can see notification in animation in Output window, I don't understand why should it be dependent to notification about change in TestType column as you wrote.
I have done one more test and I am confused little more. I added new subitem manually bypassing the internal handling in mycollection, and it's correctly shown. Why adding new subitem by EditorItems.Add() works from here, and not from Items_ListChanged event handler? In both cases, subitems are added to BindingList.
private void tsBtnTestAdd_Click(object sender, EventArgs e)
{
var ed = items.EditorItems.Where(o => o.IsItem && (o.TestType == TestType.TestZatky)).Last();
items.EditorItems.Add(new TestParEditorItem(new TestParSubItem(), 501, ed.GridID, "Test added"));
}
But, when I first change test type, for example that P3 from AbsVyska to TestZatky, which will add two new subitems, which are not shown due to this problem we are trying to find, the same call to items.EditorItems.Add with end with strange exception originating from GridView ListChanged handler - ArgumentOutOfRangeException, Index must be within the bounds of the List. I have attached exception details with stack trace, also modified project, TelerikTestReal.
So, I don't know. Yes, I can call
ResetBindings() in ListChanged event for TestType, as you proposed, but I
have any clue why it doesn't work without it. Your explanation about notifications doesn't have sense to me, because I think I can see all necessary ListChanged notifications.
Hi Marian,
Thank you for your patience and understanding.
I have examined the reported behavior and the mentioned exception. After debugging the exception and examining the scenario, indeed all list changed events are called as expected by the BindingList. However, the internal RadListSource collection of the RadGridView which is used for data operation is not updated in this particular case. The ListChanged event is called correctly, but the internal collection does not catch the changes at that moment. That is why ArgumentOutOfRangeException is thrown when we try to remove an item from the BindingList. This item is present in the bound collection but not in the internal one.
I can agree with you that this needs to be handled on our side. That is why I have logged it on your behalf in our Feedback Portal where you can track its progress. There you can follow the item and receive status notification changes.
So far, what I can suggest as a workaround is what you already know. You can call the ResetBindings() method of the BindingList which will trigger the RadGridView to update its internal collection.
I want to thank you for your time in reporting this behavior and sharing the project which reproduces it. I have updated your Telerik Points for your effort.
Regards,
Dinko | Tech Support Engineer
Progress Telerik
Hello,
thanks, I am very glad you finally found the problem, this was important for me and I was sure there must be some problem in ListChanged handling. Maybe the problem is in changing items inside of ListChanged event handler, so GridView gets nested ListChanged event, and maybe ignores second event while processing the first one?
Ok, so I will use ResetBindings() for now.
Hello, I have found one little problem in this, when I call ResetBindings(), all main items are collapsed. What can I do to maintain previous collapse/expand status of items?
Indeed, when the ResetBinding() method is called the control will be rebind thus all rows collapsed. What you can try to is to store the expanded rows and expand the same again when the binding is reset. First to catch the moment when a row is expanded, subscribe to the ChildViewExpanded event. In the event handler, store each expanded row. The next step is to subscribe to the ListChanged event of the EditorItems and catch the moment when the ListChangedType property is reset. Then used the stored collection to restore the expanded rows.
radGridView1.DataSource = items.EditorItems;
this.radGridView1.ChildViewExpanded += RadGridView1_ChildViewExpanded;
foreach (var row in radGridView1.Rows)
row.IsExpanded = true;
items.EditorItems.ListChanged += EditorItems_ListChanged;
private void RadGridView1_ChildViewExpanded(object sender, ChildViewExpandedEventArgs e)
{
if (e.IsExpanded)
{
this.expandedRows.Add(e.ParentRow);
}
else
{
this.expandedRows.Remove(e.ParentRow);
}
}
private void EditorItems_ListChanged(object sender, ListChangedEventArgs e)
{
if (e.ListChangedType == ListChangedType.Reset)
{
foreach (var item in radGridView1.Rows)
{
var value = item.Cells[0].Value;
var row = expandedRows.FirstOrDefault(x => x.Cells[0].Value == item.Cells[0].Value);
if (row != null)
{
item.IsExpanded = true;
}
}
}
}
I am also attaching the TelerikTestReal project with the above approach. You could extend this approach to fit in your application or use it as a base and implement your own. However, in general, the approach will need to manually set the IsExpanded property of the rows after the reset operations.
thanks, I will try it. I had a similar idea, but I have little problem, or doubt, whether it will work properly. In your suggested solution, you attach ListChanged handler to same event as GridView. And now, which handler will be called first? What if I will first update all IsExpanded properties, and GridView will handle Reset later?
So I tried it. Calling order of event handlers is probably defined by the order of attaching handlers. I don't know if I can rely on it, but it's working. Now I have it like this:
radGridView1.DataSource = items.EditorItems; items.EditorItems.ListChanged += EditorItems_ListChanged;
But when I change the order of these two lines, it's no longer working. I have slightly modified event handler, I have list of expanded bound items.
private void radGridView1_ChildViewExpanded(object sender, ChildViewExpandedEventArgs e)
{
var ed = e.ParentRow.DataBoundItem as TestParEditorItem;
if (ed == null)
return;
if (e.IsExpanded)
expandedRows.Add(ed);
else
expandedRows.Remove(ed);
}
private void EditorItems_ListChanged(object sender, ListChangedEventArgs e)
{
if (e.ListChangedType == ListChangedType.Reset)
{
foreach (var row in radGridView1.Rows)
{
var ed = row.DataBoundItem as TestParEditorItem;
if (!ed.IsItem)
continue;
if (expandedRows.Contains(ed))
row.IsExpanded = true;
}
}
}
You are in the right direction. The order of the event handlers is defined by the other of the subscription. Indeed, changing the line order will mess up the logic. This is because they are still not reloaded and there are still expanded rows. After reviewing the code I think a better place will be after calling the ResetBindings() method. Here is the updated code:
private void tsBtnRefresh_Click(object sender, EventArgs e)
{
items.EditorItems.ResetBindings();
foreach (var item in radGridView1.Rows)
{
var row = expandedRows.FirstOrDefault(x => x.Cells[0].Value.ToString() == item.Cells[0].Value.ToString());
if (row != null)
{
item.IsExpanded = true;
}
}
}
private void RadGridView1_ChildViewExpanded(object sender, ChildViewExpandedEventArgs e)
{
if (e.IsExpanded)
{
if (this.expandedRows.FirstOrDefault(x => x.Cells[0].Value.ToString() == e.ParentRow.Cells[0].Value.ToString()) == null)
{
this.expandedRows.Add(e.ParentRow);
}
}
else
{
var rowToRemove = this.expandedRows.FirstOrDefault(x => x.Cells[0].Value.ToString() == e.ParentRow.Cells[0].Value.ToString());
if (rowToRemove != null)
{
this.expandedRows.Remove(rowToRemove);
}
}
}
You could update the approach in ChildViewExpanded and in the Click event handler to use data bound object instead of checking the first cell value.