Search code examples
c#wpfprism

WPF/Prism: Access to the same VM-instance from two different views


I am currently working on my first Prism project and am stuck on the following problem:

My project consists of two regions, a ContentRegion and a MenuRegion, each of which should access the same instance of the ViewModel.

Within the MenuRegion some methods of the active ViewModel should be selectable. In the example below, the Save method should be able to be fired from both the ContentRegion and the MenuRegion.

The problem is that I initially create two different instances of ContentAViewModel and the Save method of the menu cannot access the current data of my ContentRegion.

I've tried registering the ViewModels as singletons, but unfortunately that doesn't work and probably violates Prism principles.

public partial class App
{
        protected override Window CreateShell()
        {
            return Container.Resolve<MainWindow>();
        }

        protected override void RegisterTypes(IContainerRegistry containerRegistry)
        {
            
        }

        protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
        {
            moduleCatalog.AddModule<ModuleA>();
        }
}
public class MainWindowViewModel : BindableBase
{
        private readonly IRegionManager _regionManager;

        public MainWindowViewModel(IContainerExtension container, IRegionManager regionManager)
        {
            _regionManager = regionManager;

            _regionManager.RequestNavigate("ContentArea", "ContentAView");
            _regionManager.RequestNavigate("MenuArea", "MenuA");
        }
}

<Window
    x:Class="PrismProject.Views.MainWindow"
    xmlns:prism="http://prismlibrary.com/"
    AllowsTransparency="True"
    Background="Transparent"
    xmlns:core="clr-namespace:PrismProject.Core;assembly=PrismProject.Core"
    ResizeMode="CanResizeWithGrip"
    WindowStyle="None">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>

       <Grid>
           <Grid.ColumnDefinitions>
               <ColumnDefinition Width="50" />
               <ColumnDefinition Width="300" />
               <ColumnDefinition Width="*" />
           </Grid.ColumnDefinitions>

          <!--[...]-->

           <ContentControl
               Grid.Column="1"
               Panel.ZIndex="10"
               prism:RegionManager.RegionName="{x:Static core:RegionNames.MenuRegion}" />

          <!--[...]-->
       </Grid>
       
       <ContentControl
           Grid.Row="1"
           prism:RegionManager.RegionName="{x:Static core:RegionNames.ContentRegion}" />
</Window>

public class ModuleA : IModule
{
        private readonly IRegionManager _regionManager;

        public CalculatingModule(IRegionManager regionManager)
        {
            _regionManager = regionManager;
        }

        public void OnInitialized(IContainerProvider containerProvider)
        {
            _regionManager.RegisterViewWithRegion(RegionNames.ContentRegion, typeof(ContentAView));
            _regionManager.RegisterViewWithRegion(RegionNames.ContentRegion, typeof(ContentBView));
            _regionManager.RegisterViewWithRegion(RegionNames.MenuRegion, typeof(MenuA));
            _regionManager.RegisterViewWithRegion(RegionNames.MenuRegion, typeof(MenuB));
        }

        public void RegisterTypes(IContainerRegistry containerRegistry)
        {
            //containerRegistry.RegisterSingleton<ContentAViewModel>();
            //containerRegistry.RegisterSingleton<ContentBViewModel>();

            ViewModelLocationProvider.Register<MenuA, ContentAViewModel>();
            ViewModelLocationProvider.Register<MenuB, ContentBViewModel>();
        }
}
public class ContentAViewModel: RegionViewModelBase
{
        private readonly IRegionManager _regionManager;

        public ContentAViewModel(IRegionManager regionManager) : base(regionManager)
        {
            SaveCommand = new(Save);
            _regionManager = regionManager;
        }
        
        private void Save()
        {
            // logic
        }
}

<UserControl
    x:Class="PrismProject.Modules.ModuleA.Views.ContentAView"
    xmlns:prism="http://prismlibrary.com/"
    prism:ViewModelLocator.AutoWireViewModel="True">
    <Grid>
       <Button 
          Command="{Binding SaveCommand}"
          Content="Save"/>

          <!--[...]-->
    </Grid>
</UserControl>
<UserControl
    x:Class="PrismProject.Modules.ModuleA.Menus.MenuA"
    xmlns:prism="http://prismlibrary.com/"
    prism:ViewModelLocator.AutoWireViewModel="True">
    <Grid>
       <Menu>                
          <MenuItem
              Command="{Binding SaveCommand}"
              Header="Save"/>

          <!--[...]-->
       </Menu>
    </Grid>
</UserControl>

How can I assign the same ViewModel to multiple views while working with only one instance of the ViewModel?


Solution

  • I have to do this same thing. I used an elegant technique presented by Brian Lagunas (one of the authors of Prism) in one of his online videos. Is from an excellent tutorial site that I will not name so as not to sound like a shill but if you google "Prism Problems & Solutions: Loading Dependent Views" you will find it. I strongly recommend watching it. But I will give you the outline of the technique here.

    (In my case, each of my module's main views needed to share its view model with a "Tools" view that Prism needed to automatically put into another region I defined)

    1. Create an attribute (I called it the PageToolAttribute) that takes the name of the "companion" view
    2. Apply the attribute to your "main" view that should share its view-model with the companion view. Supply name the "companion view you want to appear in the other Prism region.
    3. Create a class derived from Prism's RegionBehavior class that (in its override of OnAttach) hooks on to the Prism Region's ActiveViews.CollectionChanged event. In the handler you ask the newly added view if it has that PageToolAttribute.
    4. If the newly added view does support the attribute, your event hander gets its name and creates a new instance of it, gives it the same DataContext as the view (i.e., the view-model you want to share) and finally navigates the other region to the newly added view.
    5. Finally, in your Prism Aap, you override the ConfigureDefaultRegionBehaviors function and call AddIfMissing (on the supplied IRegionBehaviorFactory) to add your attribute.

    It is a lot to take in and requires a bit of a deep dive into Prism but a surprisingly small amount of code and it works like a champ. I now have 7 different Modules, each with its own companion "Tool" view that gets navigated each time.