Prism v4 WPF 4 ItemsControl region, register multiple views of same type

Topics: Prism v4 - WPF 4
Jun 7, 2011 at 9:47 PM

Dear experts,

My goal is to assign a region to an itemscontrol and populate this region with multiple instances of the same view, each instance contains different data from the underlying model. I'm working according to the MVVM pattern, so my view has an associated view model and a model. To make things more concrete I will explain my situation by an example.

Consider a canvas in which I want to show rectangles. Because each rectangle has quite some logic I didn't want to put all this in a DataTemplate. Therefore I created a view and viewmodel that define a single rectangle. At first the ViewModel had a constructor which has a 'Model model' parameter, but as MEF is unable to import views that require a parameter this could not hold. When reading through the documentation I saw that it is possible to pass parameters using the RequestNavigate() method so I went ahead and implemented it this way:

        <ItemsControl
            Style="{StaticResource NoScrollViewerItemsControlStyle}"
            prism:RegionManager.RegionName="TestRegion"
            />

The NoScrollViewerItemsControlsStyle modifies the itemscontrol in such a way that it is possible to place the rectangle views on a canvas.

Now to populate this items control I used:

 

foreach(var rectangle in RectangleModel.Rectangles)
_regionManager.RequestNavigate("TestRegion", new Uri("RectangleView?Id=" + rectangle.Id, UriKind.Relative));

 

Now by implementing the INavigationAware interface on the RectangleViewModel it is possible to pass the right model to the RectangleViewModel by implementing the OnNavigatedTo method correctly.

Before I started doing that i first implemented the IsNavigationTarget as follows

        public bool IsNavigationTarget(NavigationContext navigationContext)
        {
            return false;
        }
to make sure the itemscontrol gets filled with all of my RectangleViews. (If i did not do this only one view was added)

But it isn't :-(. It comes up with the following error:

View already exists in region.

Q: What am I doing wrong? I guess it should be possible to load multiple views into the itemscontrol


 Many thanks in advance!

Jun 8, 2011 at 1:27 PM

Ok I'm a bit further now.

I went on reading the documentation after feeling bad about using Navigation techniques to populate the itemscontrol, because if you think about it it isn't really navigation. It was only some sort of hack because MEF's view discovery doesn't support loading views who's viewmodel requires parameters. (In my case the associated model). I found the following passage [Page 91 of Prismv4.pdf documentation]:

Use view injection in the following situations:
(...)
You need to display multiple instances of the same views in a region, where each view instance is bound to different data.
(...)

So view injection is the technique I'm looking for. Unfortunately the documentation only requests a view from the container by talking to the container directly as shown below:

 

// View injection
IRegion region = regionManager.Regions["MainRegion"];
var ordersView = container.Resolve<OrdersView>();
region.Add(ordersView, "OrdersView");
region.Activate(ordersView);

Maybe I'm wrong but having the container reference published throughout the application for this kind of stuff doesn't feel right. I searched this forum for a solution and found the following:
http://compositewpf.codeplex.com/discussions/239160

 

They use the ServiceLocator to obtain a view from MEF. With this my actual code became:

 

            foreach (IRectangle rectangle in modelSource.Rectangles)
            {
                var view = ServiceLocator.Current.GetInstance<RectangleView>();
                view.ViewModel.Model = rectangle;
                IRegion region = _regionManager.Regions["TestRegion"];
                region.Add(view);
            }

 

Now, as expected setting a breakpoint on the IRegion = region line I see a instance requested via MEF of RectangleView of which the ViewModel is property is populated by MEF also, great!
The Model propety of the ViewModel is also set.

But now I still get the same warning: View already exists in region. when the second rectangle view is added to the itemscontrol!?! I don't get it!

The documentation even has an example in which they do roughly the same (Page 296):

ActivityView activityView1 = Container.Resolve<ActivityView>();
ActivityView activityView2 = Container.Resolve<ActivityView>();
activityView1.CustomerId = "Customer1";
activityView2.CustomerId = "Customer2";
IRegion rightRegion = RegionManager.Regions["RightRegion"];
rightRegion.Add(activityView1);
rightRegion.Add(activityView2);

What goes wrong?

Developer
Jun 8, 2011 at 1:39 PM

Hi,

The problem you're experiencing might be caused by the fact that you have registered your RectangleView as a singleton instance.

When you call the RequestNavigate method, the navigation mechanism internally uses the service locator to retrieve an instance of your view. If you are using MEF and you have registered your view as a Shared instance (which is the default behavior the Export attribute has), the service locator will retrieve the same instance on each request, thus causing the error you're experiencing.

You could try decorating your class with the PartCreationPolicy attribute, and specify that the creation policy is NonShared. To illustrate this:

 

[Export("RectangleView")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class RectangleView

{

(...)

}

I hope you find this helpful.

Guido Leandro Maliandi
http://blogs.southworks.net/gmaliandi

Jun 8, 2011 at 2:16 PM

Hello mister Maliandi!

Thank you very much for your reply. This was indeed the problem and it's now working as expected!! :-) Woehoe!!

I stay with the view-injection approach as it is better than 'misusing' the Navigation functionality for my goal.

The only 'problem'  I now experience with view injection as compared to view discovery is that the _regionManager.RegisterViewWithRegion() function waits untill the region is added to visual tree before it applies view discovery. By using view injection, and thus directly assigning views by calling _regionManager.AddToRegion() I have to do this myself. What i have came up with is the following:

 

_regionManager.Regions.CollectionChanged += Regions_CollectionChanged;
        void Regions_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                foreach (var newItem in e.NewItems)
                {
                    IRegion addedRegion = (IRegion) newItem;
                    if (addedRegion.Name == "TestRegion")
                    {
                        populateViews();
                    }
                }

            }
        }

        private void populateViews()
        {
            foreach (IRectangle rectangle in modelSource.Rectangles)
            {
                var view = ServiceLocator.Current.GetInstance<RectangleView>();
                view.ViewModel.Model = rectangle;
                IRegion region = _regionManager.Regions["TestRegion"];
                region.Add(view);
            }
}

Is this the recommended approach for view injection or am i missing something trivial?

 

Thanks!

 

Developer
Jun 8, 2011 at 5:01 PM

Hi,

The approach you're following is a valid possiblity to achieve the proposed scenario using view injection.

I hope you find this helpful.

Guido Leandro Maliandi
http://blogs.southworks.net/gmaliandi

Jun 8, 2011 at 6:20 PM
Edited Jun 8, 2011 at 6:22 PM

Thank you very much again.

I stumbled upon another issue I need some hints/tips for. With my itemscontrol now correctly filled I want to create another itemscontrol that is filled with the same views. The purpose of this new itemscontrol is to be scaled such that all rectangles fit into a small overview window:

 

            <ItemsControl
                Style="{StaticResource NoScrollViewerItemsControlStyle}"
                prism:RegionManager.RegionName="MainRegion" <!-- Original TestRegion -->
                />

            <!-- The code to resize this itemscontrol (and with that all it's containing items) down is in place and working correctly -->
            <ItemsControl
                Style="{StaticResource NoScrollViewerItemsControlStyle}"
                prism:RegionManager.RegionName="OverviewRegion"
                />

 

Now to fill both itemscontrol with the same views my approach is as follows:

 

        private void PopulateViews()
        {
            IRegion mainRegion = _regionManager.Regions["MainRegion"];
            IRegion overviewRegion = _regionManager.Regions["OverviewRegion"];

            foreach (IRectangle rectangle in modelSource.Rectangles)
            {
                var view = ServiceLocator.Current.GetInstance<RectangleView>();
                view.ViewModel.Model = rectangle;
                mainRegion.Add(view);
                overviewRegion.Add(view);
            }
        }

I also adjusted Regions_CollectionChanged to start PopulateViews() only when both mentioned regions are registered in the visual tree.

Unfortunately the result is that only the region to which the views have been added as last (in this case the OverviewRegion) will show the views, so the MainRegion becomes empty.
I tried added 'mainRegion.Activate(view)', but this has no effect. The weird thing is that when debugging I do see all the rectangles present in both mainRegion.ActiveViews and overviewRegion.ActiveViews. When i alter the method to the following:

 

        private void PopulateViews()
        {
            IRegion mainRegion = _regionManager.Regions["MainRegion"];
            IRegion overviewRegion = _regionManager.Regions["OverviewRegion"];

            foreach (IRectangle rectangle in modelSource.Rectangles)
            {
                var view = ServiceLocator.Current.GetInstance<RectangleView>();
                view.ViewModel.Model = rectangle;
                mainRegion.Add(view);

                var view2 = ServiceLocator.Current.GetInstance<RectangleView>();
                view2.ViewModel.Model = rectangle;
                overviewRegion.Add(view2);
            }
        }

It works but this 'fix' means having a lot unnecessarily view and viewmodel copies.

 

Do i have to work with scoped regions for this to work? I read about them in the manual but they do not seem to apply to my situation.

Hope someone can help!

Many thanks.

Note: If someone feels this has to be in a separate thread I will make one, but I also do not want to spam these forums :-)

Developer
Jun 8, 2011 at 6:52 PM

Hi,

It's OK to post the question in this thread. You can't have the same instance of a view added two times to the visual tree. In order to achieve the scenario you're mentioning, you need to add two different instances of the view.

I hope you find this helpful.

Guido Leandro Maliandi
http://blogs.southworks.net/gmaliandi

Jun 8, 2011 at 7:20 PM
Edited Jun 8, 2011 at 7:48 PM

Thanks again for your quick response.

Hmmm interesting, are the views added to the visual tree when assigning them to a region? I thought, at least in case of the itemscontrol, they get collected in the Region.Views property, then, when activated added to the Region.ActiveViews property and this is somehow bound to the itemscontrol ItemsSource property. [This could explain why the ItemsSource must be unspecified when defining a region.]

In my situation adding different instances of the views to each usercontrol will mean a lot of extra objects. Therefore I ditched the regions for the itemscontrols and instead created a public ObservableCollection property which gets filled with the views by view injection (is this still called view injection now?).

 

        private void PopulateViews()
        {
            foreach (IRectangle rectangle in modelSource.Rectangles)
            {
                var view = ServiceLocator.Current.GetInstance<RectangleView>();
                view.ViewModel.Model = rectangle;
                MyObservColl.Add(view);
            }
        }

 

This new property is then bound to the ItemsSource of both itemscontrols. This works ok, i think having regions for my situation was a little bloated after all as the regions would never contain other views than these rectangles.

I am still curious what you think about this final solution.

Many thanks

 

------- EDIT ------

Whoops, I am terribly sorry. Now that i have worked it out completely it seems like binding the same ObservableCollection to both itemcontrols ItemsSource also only let the last bounded itemscontrol view the items. The only solution is to don't share the collection and thus making a new one for the second itemscontrol. You were right... SORRY.

I guess i will have to go that way then, I will stay with data binding instead of using regions for the aforementioned reason.