Multiple Regions sharing one view/viewmodel but each view filtered.

Topics: Prism v4 - Silverlight 4
May 15, 2011 at 2:41 PM

Hello All, I am currently at a loss for the best way to approach this.  Using the UI Composition QuickStart as a guide I am trying to basically create three regions each using the same view and viewmodel but showing different data based on a filter of Status.  I am using MEF and I am setting the creation policy to non-shared so I get multiple instances of the view.  I am also using event aggregation as in the UI Composition example to pass data along.  In this case a selected Project.  So far I have everything working except the additional filtering of the views in each region.

Here are the objects:

public enum Status {NotStarted, InProgress, Complete};

public class Task
{
public int Id {get;set;}
public string Name {get;set;}
public int ProjectId {get;set;}
public Status Status {get;set;}
}

public class Project
{
public int Id {get;set;}
public string Name {get;set;}
}

public class TaskCollection : ObservableCollection<Task>
{ }

I have a "SummaryView" like the UI Comp. example which holds each region:

<ItemsControl x:Name="NotStartedTasksItemsControl"
  prism:RegionManager.RegionName="NotStartedTasksRegion"
  prism:RegionManager.RegionContext="{Binding CurrentProject}"/>
<ItemsControl x:Name="InProgressTasksItemsControl" 
prism:RegionManager.RegionName="InProgressTasksRegion" prism:RegionManager.RegionContext="{Binding CurrentProject}"/> <ItemsControl x:Name="CompletedTasksItemsControl"
prism:RegionManager.RegionName="CompletedTasksRegion" prism:RegionManager.RegionContext="{Binding CurrentProject}"/>

I am creating regions in ModuleInit.cs as follows:

_regionManager.RegisterViewWithRegion(RegionNames.NotStartedTasksRegion, () => _serviceLocator.GetInstance{TaskListView}());
_regionManager.RegisterViewWithRegion(RegionNames.InProgressTasksRegion, () => _serviceLocator.GetInstance{TaskListView}());
_regionManager.RegisterViewWithRegion(RegionNames.CompletedTasksRegion, () => _serviceLocator.GetInstance{TaskListView}());

{} == < and >

As you can see I am passing the current selected Project to each Region via regioncontext.  And using a similar technique to the ui comp. example I do indeed get each region with the list of Tasks for the project.  But I would like to filter each region / view based on the status.  Right now it simply repeats the same items in each region. 

It is possible my current implementation is wrong and that I should be doing this another way.  If so, please let me know.  But so far I could not find a way to pass additional information to filter the results by status.  I really do not want to have to create a view/viewmodel for each status, that seems too much.  I had thought about using some type of service/controller to pass the additional information, but I would need some way to "trigger" this passing of information. 

Some other thoughts that might work:

1. using datatemplate on the tasklist view to somehow filter. 
2. some how pass more than selected project in regioncontext? but how to make it know which status to filter on got me stumped.
3. build time machine to go to future in which I have the answer then go back in time to implement it.

If anyone has thoughts on how to best approach this I would be very appreciative.  Also, If there is any additional information required please let me know and I will provide it.

Thank you.

May 15, 2011 at 6:26 PM

Update:  Ok I was able to get this to work but it is really hacky and I am not sure if this adheres to the MVVM pattern.

What I am doing is using the event PropertyChanged event on the RegionContext and search out the region name.  I then convert the name to the status enum and pass it to the viewmodel.  Because this happens before the actual processing of the datacontext it works.  But as I said, I do not like this and hope someone else has a better idea.

In the ViewModel code behind:

RegionContext.GetObservableContext(this).PropertyChanged += new PropertyChangedEventHandler(TaskListView_PropertyChanged);
void TaskListView_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
            var r = (RegionManager.GetObservableRegion(this.Parent).Value).Name;
            Status status = r.Contains("NotStarted") ? Status.NotStarted : r.Contains("InProgress") ? Status.InProgress : r.Contains("Complete") ? Status.Complete : Status.NotStarted;
            ViewModel.Status = status; // <-- Must be before setting Project or it will not filter on this additional criteria.
            ViewModel.CurrentProject = RegionContext.GetObservableContext(this).Value as Project;
}

I wish there was a more elegant way to pass this additional criteria to the view/viewmodel that is shared among the three regions.  Once again any thoughts, help, or information is much appreciated.

May 16, 2011 at 6:25 AM

I think you are trying to do this

- Load a project

- Assign it as the region context for each region

- Each region filters the tasks in the project based on status

The best way to achieve this is using the INavigationAware interface. Since you are using MEF you have an export on the view like below. Your view model implement this interface. And you would load the views in your region as follows

[Export("MyView")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public partial class MyView : UserControl
{
   [ImportingConstructor]
   public MyView(IMyViewModel viewModel)
   {
       InitializeComponent();
       DataContext = viewModel;
   }
}

[Export(typeof(IMyViewModel))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class MyViewModel : IMyViewModel, INaviagtionAware
{
   private string _status;

  public void OnNavigatedTo(NavigationContext navigationContext)
{
  _status = navigationContext.Parameters["Status"];
}
 public bool IsNavigationTarget(NavigationContext navigationContext)
        {
            return true;
        }

        public void OnNavigatedFrom(NavigationContext navigationContext)
        {
            
        }
}

[ModuleExport("MyModule", typeof(Module))]
public class Module : IModule
    {
        public void Initialize(IRegionManager regionManager)
        {
            _regionManager.RequestNavigate(RegionNames.NotStartedTasksRegion, new Uri("MyView" + new UriQuery {{"Status", Status.NotStarted}}, UriKind.Relative));
_regionManager.RequestNavigate(RegionNames.InProgressTasksRegion, new Uri("MyView" + new UriQuery {{"Status", Status.InProgress}}, UriKind.Relative));
_regionManager.RequestNavigate(RegionNames.CompletedTasksRegion, new Uri("MyView" + new UriQuery {{"Status", Status.Completed}}, UriKind.Relative));
        }
    }

If you see above in the Module Initialize I'm using RequestNavigate and loading views into the regions. Here you can make use of the UriQuery to pass parameters to each view/viewmodel which implements INaviagtionAware. The above code will create new instances of the view/view model in the regions and pass different parameters to them. Now in your view model OnNavigatedTo you will pick the Status from the navigationContext and apply that as a filter on your regionContext (CurrentProject).

Hope this helps.

Cheers.

May 16, 2011 at 1:47 PM

Hello gan_s,

Thank you for your reply.  I did not think about using INavigationAware.  For some reason I was stuck on one way of doing things.  I did try to implement your idea and initially it did not work.  But then I moved the calls to RequestNavigate to my controller class and it worked?  I am not sure why that would be.  The controller class is very similar to the MainRegionController.cs class in the UI Composition QuickStart.  I added the calls to RequestNavigate right after setting the view's datacontext:

//...
// Set the current project property on the view model.
TaskSummaryViewModel viewModel = view.DataContext as TaskSummaryViewModel;

if (viewModel != null)
{
    viewModel.CurrentProject = selectedProject;
}
            this.regionManager.RequestNavigate(RegionNames.NotStartedTasksRegion, new Uri("TaskListView" + new UriQuery { { "Status", TaskStatus.NotStarted.ToString() } }, UriKind.Relative));
//...

Not sure why it would work here, but not in the ModuleInit class.  Will need to do some digging.  But thank you for this information, definitely feels better than the method I was using.

aromano

May 16, 2011 at 3:19 PM

Glad it works for you. To debug inject the aggregatecatalog in the Module constructor. Then before doing the RequestNavigate in the Initialize just check the Parts in the aggregatecatalog that have been downloaded. If your view you are trying to navigate to does not show up in it then it probably hasnt been composed yet and thats why you're RequestNavigate did not work in the Initialize. If not then something to look further into :) !