Remove View from AllActiveRegion does not work.

Topics: Prism v4 - WPF 4
Dec 27, 2012 at 2:13 PM
Edited Dec 27, 2012 at 2:14 PM

Hello, 

I have ItemsControl as a Region in my Prismv4 WPF app. In this region I want to add multiple instances of the same View... so basicly I do something like this:

 

                var id = DateTime.UtcNow.Ticks;
                var uriQuery = new UriQuery
                    {
                        { "ID", id.ToString(CultureInfo.InvariantCulture) },
                    };
                this.regionManager.RequestNavigate(
                    RegionNames.MyRegion, new Uri(ViewNames.MyView + uriQuery.ToString(), UriKind.Relative));

Now what is the problem... I also want to remove this Views:

In their ViewModels I do this:

 

            var views = this.regionManager.Regions[RegionNames.MyRegion].Views;

            foreach (var view in views)
            {
                var myView = (IMyView)view;
                if (myView.ViewSortHint == this.ID)
                {
                    this.regionManager.Regions[RegionNames.MyRegion].Remove(view);
                }
            }

 

Well it works if there are no more than 2 Views in MyRegion. When I add three the first one is removed and second one throws InvalidOperationException Added item does not appear at given index '0'. at line 

this.regionManager.Regions[RegionNames.MyRegion].Remove(view);

I failed to find the solution. It drives me absolutely crazy!

Developer
Dec 27, 2012 at 5:21 PM

Hi,

Based on my understanding, this kind of problems seems to appear when modifying a collection of items, while iterating over it using a foreach statement. In this case, you are iterating over the Views collection of the region using a foreach and modifying it inside by removing a view from the region.

This is mentioned in the following MSDN article:

A quick approach to avoid this kind of problems is to "find" what elements you want to remove (in this case a view) from the collection and remove them outside the foreach statement. For example:

var views = this.regionManager.Regions[RegionNames.MyRegion].Views;
object viewToRemove = null;

foreach (var view in views)
{
    var myView = (IMyView)view;
    if (myView.ViewSortHint == this.ID)
    {
        // We found the view we want to remove
        viewToRemove = myView;
    }
}

if(viewToRemove != null)
{
    // We remove the view outside the foreach
    this.regionManager.Regions[RegionNames.MyRegion].Remove(viewToRemove);
}

Another approach could be to use a for loop instead of a foreach loop.

I hope you find this useful,

Damian Cherubini
http://blogs.southworks.net/dcherubini

Jan 2, 2013 at 10:00 AM
Edited Jan 2, 2013 at 10:20 AM

Hi, 

Thank you very much for your reply. I am sorry that I react this late. I forgot to mention this, but I also tried

            var views = this.regionManager.Regions[RegionNames.MyRegion].Views.Select(v => (IMyView)v);
            var viewToRemove = views.SingleOrDefault(v => v.ViewSortHint == this.ID);
            
            if (viewToRemove != null)
            {
                this.regionManager.Regions[RegionNames.MyRegion].Remove(viewToRemove);
            }
It's basicly the code you provided. No foreach. And it had same result. When I add two views to MyRegion(ItemsControl) it works fine. When there are 3 or more views first is removed and second throws InvalidOperationException Added item does not appear at given index '0'. UPDATE I also tried your code and it does not work either. Only thing comes to my mind. I did not say that in that region I have custom SortComparison method. i don't know if it's somehow important.
Developer
Jan 2, 2013 at 2:08 PM

Hi,

So far I couldn't reproduce this behavior, perhaps if you are able to reproduce this undesired behavior in isolation, it would be helpful if you could provide us with a repro sample application; so that we can help you find the underlying cause behind it.

Regards,

Agustin Adami
http://blogs.southworks.net/aadami

Jan 3, 2013 at 3:52 PM

Hello,

So here it is... It behaves exactly like I described...

Application have single button and in it's command I navigate to NotificationRegion. (please look at "TestViewModel.cs" and "NotifyCommand") There is a timer in target View's ViewModel. NotificationView gets removed from NotificationRegion after 5 seconds. Timer can be easily substituted with command(it's also there, but it's commented out, look into NotificationViewModel.cs) Like I said before... unless I add more than two NotificationViews to NotificationRegion everything is OK. Thank you very much for your time!

Viktor La Croix

Developer
Jan 3, 2013 at 9:44 PM

Hi Viktor,

We have downloaded your repro-sample application and we could reproduce the exception you are mentioning.

After analyzing your sample, we found that indeed the problem is related to your custom SortComparison approach (if this method is not used, the exception does not appear.) However, we are not sure if this problem is only related to the logic behind the sorting method or if it is also a bug in the Prism library where this kind of scenarios are not properly supported.

Basically, the cause behind this exception is this:

  • When a view is added in the NotificationRequest the Region automatically re-sorts its views by using your custom SortComparison method. However, during this operation, the ViewSortHint property of the newly added view will return null. Based on my understanding, this happens because the OnNavigatedTo method of the view model (where the ID property used for the ViewSortHint is set) is invoked after the views are ordered. Hence, during the sorting process the new view has no ViewSortHint, and it would be sorted incorrectly.
  • When a second view is added in the region the same happens, with the difference that the first view now has its corresponding ViewSortHint property set. Hence, for several views, all view are correctly sorted with the exception of the new one.
  • When removing views from a Region, Prism first obtains the corresponding index for the view inside the view collection. Then (for some reason) Prism sort its collection of views again and after that, it removes the views using the index. Here is where the problem arises. As the views were not completely sorted, due to the behavior described above, the views are re-sorted (as now the last view has its ViewSortHint set) and therefore, their indexes are changed. When trying to remove the view the index is no longer valid, throwing an exception.

As complex as this may sound, a possible work-around to avoid this behavior could be to re-sort the collection of views before trying to remove a view. However, currently this cannot be done manually, so you will need to force the sorting logic of the Region to execute indirectly. A possible approach to do this is to change the SortComparison property of the Region like this:

// In the beginning of the TimerOnTick method of the NotificationViewModel class
regionManager.Regions[RegionNames.NotificationRegion].SortComparison = null;
regionManager.Regions[RegionNames.NotificationRegion].SortComparison = NotificationService.CompareMethod;

This will force the Region to resort the views, avoiding movements during the removal process.

We will kept analyzing this to see if creating a work item in the issue tracking section is required.

I you find this handy,

Damian Cherubini
http://blogs.southworks.net/dcherubini

Jan 4, 2013 at 3:19 PM
Edited Jan 4, 2013 at 3:55 PM

Hello, 

Thank you! It's great answer... It was really bad idea to use ticks in Message property, because I did not see it myself. First of all my SortComparison method was wrong (I wanted the oposite). I modified Message property of NotificationViewModel to be simple int number and it was obvious. Views were sorted wrong. (5 views from top to bottom 5 1 2 3 4)

I did not think of this because there was that animation in NotificationView(on Loaded) so it was visible that new View is added to the top so I thought SortComparison is ok. Thanks to those ticks in message it was the only thing I was able to check. 

With your work-around I can avoid that Exception. Sadly it's not what I want. What I want is probably clear to you now. I need new view to be added to the top of the ItemsControl and I want to remove from the bottom(FIFO) of course those views have to be sorted correctly all the time.

If there is no better solution I will probably use your workaround and modify SortComparison method to take that null value in account :)

Thank you very much.

best regards Viktor La Croix

Developer
Jan 4, 2013 at 6:50 PM

Hi Viktor,

I am glad you found this useful.

In my opinion, a possible approach to show the NotificationViews properly ordered could be to move the workaround from the TimerOnTick method, to the end of the OnNavigatedTo method in the NotificationViewModel class:

public void OnNavigatedTo(NavigationContext navigationContext)
{
    this.ID = navigationContext.Parameters["ID"];
    this.Level = navigationContext.Parameters["Level"];
    this.Message = navigationContext.Parameters["Message"];

    // Workaround
    regionManager.Regions[RegionNames.NotificationRegion].SortComparison = null;
    regionManager.Regions[RegionNames.NotificationRegion].SortComparison = NotificationService.CompareMethod;
}

Following the logic described above, when a new view is added first the NotificationViews are ordered incorrectly, then the OnNavigatedTo method of the new view is invoked where the ID property is set at then the workaround is applied forcing the views to be re-sorted. This second time the views are ordered correctly as the ID property is now available. The effect is that the views are shown in order from "the beginning."

I hope this helps,

Damian Cherubini
http://blogs.southworks.net/dcherubini

Jan 7, 2013 at 10:55 AM

Thank You All very much... I will do as You suggested. But still I would like to know if there will be something done about this. 

Best regards, Viktor La Croix