1
votes

I'm writing a WPF application using Prism and facing the following dilemma: I have a view which exposes 2 regions, one for a Grid an the other for a filter panel, which enables advanced filtering of the items in the grid.

Those views are discovered by Prism and instantiated into the designated regions. When a filter is applied, the Grid has to be updated according to the appropriate filter. this can be achieved, for example, by using a Controller (if the ViewModels reside in the same module) or using Events (utilizing the EventAggragator). So far so good.

My problem is that this view may have multiple instances in different windows. I therefore need to know the context of the filter, to be able to determine which grid has to be affected. However, the ViewModel instances are unaware of each other.

How can this be best achieved ?

2
Did you check if the "RegionContext" property can be helpful? It has proven helpful in similar questions. - Gayot Fow

2 Answers

1
votes

How are you populating the two regions? Does your "parent" view explicitly call regionManager.RequestNavigate() on them, or do you "fix" the regions to their views during application startup, using regionManager.RegisterViewWithRegion()?

With the former, your parent view could (say) generate a GUID and pass this to the child views using the context parameter when you call RequestNavigate(). The views can grab this value within OnNavigatedTo(). If you are using EventAggregator your could expose the GUID as a property on the message object, and use the predicate method overload when subscribing, so the graph view only receives messages containing the expected GUID.

If you are using RegisterViewWithRegion, things get more tricky. I know you can set a context value in the region's XAML, so following the GUID idea, and assuming the VM exposes the generated GUID value through a property called MyViewGuid:-

<ContentControl Regions:RegionManager.RegionName="Foo"
                Regions:RegionManager.RegionContext="{Binding MyViewGuid}"/>

I've not found an easy way to get that context value to the child view-models (given that their INavigationAware methods don't get called in this scenario). I used a hacky approach - in the child views' constructors, set up an event handler for changes to the parent region's context value:-

var regionContext = RegionContext.GetObservableContext(this);
regionContext.PropertyChanged += RegionContextOnPropertyChanged;

The event handler would be something like this, off the top of my head:-

private void RegionContextOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
    var observableObject = sender as ObservableObject<object>;
    if (observableObject != null && observableObject.Value != null)
    {
         // Get the GUID value from the context and pass to the VM
         // (assuming the VM has a method called SetGuid().
         var myGuid = (Guid)observableObject.Value;
         (DataContext as MyViewModel).SetGuid(myGuid);
    }
}

Hopefully something I've suggested might be of use...!

0
votes

Thanks for the details answer! I have considered the suggested solutions but still uncertain about the best approach.

In general, whenever possible, I prefer view discvery over injection. In most cases I'm using "ExportView" and Import the ViewModel in the code-behind.

Regarding the Navigation approach: Using RequestNavigate() to pass the Context seems a bit tricky as it needs to be called for each of the participating views, from different modules. This means that whenever the parent view is created, an event should be published with the Context, to signal each module to perform the relevant navigation.

Regarding the RegisterViewWithRegion: Since the views don't belong to the same region, I'm not sure if the "GetObservableContext()" will work (I couldn't get it to work in my tests). In addition, I'm a bit reluctant about adding the "Context" notion and associated logic to the ViewModels.

What I was thinking is handling the Context logic in the views by:

  1. Implementing a custom Trigger which gets a PubSubEvent Type and a Context object and subscribes to that event using the context as a predicate. I can then invoke a commans in the ViewModel. It will look something like:

    <i:Interaction.Triggers>
        <Core:PubSubEventTrigger EventType="{x:Type Interfaces:FilterApplied}" 
                                 Context="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}}">
            <Core:InvokeCommandActionEx Command="{Binding FilterCommand}"/>
        </Core:PubSubEventTrigger>
    </i:Interaction.Triggers>
    
  2. Publish the PubSub event from the publisher's view using the common Context. This can be achieved using an EventTrigger and a custom Action which will publish the event. The code will lokk something like this:

    <i:Interaction.Triggers>
        <i:EventTrigger EventName="FilterChanged" SourceObject="{Binding}">
            <Core:PubSubPublishEventAction EventType="{x:Type Interfaces:FilterApplied}" Context="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    

The FilterChanged event will be raised from the ViewModel with the relevant args.

The tricky part is handling parameters: 1. Passing the params from the source ViewModel. 2. Publishing a Prism "PubSub" event with the params and the Context. 3. Stripping the Context at the Subscriber end and triggering the command with the relevant parameter.

Any insights?