Freigeben über


Working With Multiple Windows In UWP Using MVVM

Editors note: The following post was written by Windows Development MVP Ricardo Pons, as part of our Technical Tuesday series.  

I’ve been working on a LOB application for almost two years, which has many forms, charts and business rules.  A new requirement also came up, in which the application needed multiple windows that used existing views and viewmodels.

The thing was, I couldn’t find any documentation that gave me insight into how to handle my specific case. While I found this example in the official Microsoft UWP samples about how to work with multiple windows, it didn’t work for me. I am working with MVVM and Caliburn Micro as MVVM Framework. And this example doesn’t show how to work with the MVVM Pattern.

So, I decided to create my own implementation. This is how I did it:

Note: In this example, I use Microsoft Azure Cognitive Services to get images from the Bing Search API.  You can get a free trial key for one month, with more details here.

Let’s do some code!

I created the interface IWindowManagerService, where we’ll have all our methods and we can leverage it to use our service locator.

 public interface IWindowManagerService
    {

        Task ShowAsync(string childName, Type viewModelToNavigate, object parameters = null);

        List ActiveWindows { get; }
        Task CloseAsync(string childName);
        int MainViewId { get; }

        void Initialize(int mainId);

    }

Here are the methods to manage all the active windows in your application. ShowAsync is the most relevant in this case, but we’ll go through the others anyway:

ShowAsync()

This method shows your viewmodel in another Window. Parameters include:

  • ChildName: Name of the Window you want to show in another Window
  • ViewModelToNavigate: The type of the viewmodel you want to show in the window
  • Parameters: Object with some parameters you want to pass your ViewModel

CloseAsync()

This method closes a window you must specify which window you want to close. Parameters include:

  • ChildName: Name of the window you want to close

Initialize()

This method must be executed when you launch the application to save the Id of the main window. Parameters include:

  • MainId: Id of the main windows of your application

This interface has just one collection, ActiveWindows. It’s here we save all the active windows.

So to use the ShowAsync method, we need to do the following tasks:

  • Check that the window we want is not active
  • Setup the class ViewLifetimeControl taken from the example of GitHub(from Microsoft)
  • Setup the navigation of the new Window (Yes! All new windows have their own navigation service)
  • Save the id of the new Window
  • If the Window is already opened this method will inject the viewmodel and switch the window over the main window

This code does the tasks mentioned above:

       public async Task ShowAsync(string childName, Type viewModelToNavigate, object parameters = null)
        {

            var activeWindow = ActiveWindows.FirstOrDefault(x => x.Name == childName);
            if (activeWindow == null)
            {
                CoreDispatcher dispather = null;
                ViewLifetimeControl viewControl = null;
                await CoreApplication.CreateNewView().Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
                {
                    viewControl = ViewLifetimeControl.CreateForCurrentView();
                    viewControl.Title = childName;
                    dispather = viewControl.Dispatcher;
                    viewControl.Released += ViewControl_Released;
                    viewControl?.StartViewInUse();
                    Window.Current.Content = new ChildShellView();
                    ((ChildShellView)Window.Current.Content).SetupViewName(childName, viewControl);
                    ((ChildShellView)Window.Current.Content).SetupNavigationService(viewModelToNavigate, parameters);

                    int newViewId = 0;
                    newViewId = ApplicationView.GetForCurrentView().Id;
                    var navigationServiceName = "NavigationService_" + childName;
                    ActiveWindows.Add(new ActiveWindow()
                    {
                        Name = childName,
                        NavigationServiceName = navigationServiceName,
                        Id = newViewId,
                        viewControl = viewControl
                    });
                    var navigationService = IoC.Get(navigationServiceName);
                    navigationService.BackRequested += NavigationService_BackRequested;
                    navigationService.Navigated += NavigationService_Navigated;
                    Window.Current.Activate();



                });
                viewControl.StartViewInUse();

                bool viewShown = await ApplicationViewSwitcher.TryShowAsStandaloneAsync(viewControl.Id);

                if (!viewShown)
                {
                    Debug.WriteLine("La vista no se ha podido crear");
                }

                viewControl.StopViewInUse();

            }
            else
            {
                var navigationService = IoC.Get(activeWindow.NavigationServiceName);
                if (navigationService == null)
                    return;
               await activeWindow.viewControl.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () => 
                {
                    var currentSource = navigationService.SourcePageType;
                    navigationService.NavigateToViewModel(viewModelToNavigate, parameters);
                    await ApplicationViewSwitcher.SwitchAsync(activeWindow.Id, mainId, ApplicationViewSwitchingOptions.Default);
                    
                });
              
            }
           

        }

We will use a Page as container to show our ViewModel, and the View inside of this Page will use a Frame because we want to support Navigation for all of our new windows.

 <Page
    x:Class="MultiWindowExample.Views.ChildShellView"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
       <Frame x:Name="frame" DataContext="{x:Null}" />
    </Grid>
</Page>

In the code behind of our Page, we use the method SetupNavigationService. With this small bit of code, we can setup the Navigation Service for the Window:

 public void SetupNavigationService(Type ViewModelToNavigate, object parameters = null)
        {
            var shellVM = this.DataContext as ChildShellViewModel;

            shellVM.RootViewModel = ViewModelToNavigate;
            shellVM?.SetupNavigationService(frame, parameters);
        }

Thanks to the Class ViewLifetimeControl, we can see when the Window is closed. This is very useful because we can clean all the stuff we will not use anymore and prevent memory leaks.

This code cleans all the content of our page (our viewmodel and view), and closes the window.

   private void ViewLifetimeControl_Released(object sender, EventArgs e)
        {
            Loaded -= ChildShellView_Loaded;
            ((ViewLifetimeControl)sender).Released -= ViewLifetimeControl_Released;
            DataContext = null;
            var element = frame.Content as FrameworkElement;
            if (element != null)
            {

                var deactivator = element.DataContext as IDeactivate;
                if (deactivator != null)
                {
                    deactivator.Deactivate(true);
                }
                deactivator = null;
            }
            var disposable = element.DataContext as IDisposable;
            disposable?.Dispose();
            disposable = null;
            element = null;
            frame.Content = null;
            frame = null;
            GC.Collect();


            Window.Current.Close();
        }

I created a static class to save the names of my Windows. Knowing the names will enable you to find them in the collection, ActiveWindows. To do this, remember to set WindowManagerService as singleton or static. This service will show all the active windows in our application, and will save the necessary data to identify, manage and close our windows.

 public static class WindowNames
    {
        public static string CarsWindow = "CarsWindow";
        public static string BuildingsWindow = "BuildingsWindow";
        public static string PyramidsWindow = "PyramidsWindow";
    }

This is what the service looks like in the App.cs using Caliburn.Micro. I won’t talk about how to setup Caliburn.Micro in this article, but if you want to know more you can see an example here.

Now we need to inject our service and pass the Id of the main window of our application to the WindowManagerService. If the user closes the main windows, the native behavior of UWP app’s secondary windows will stay alive. In my case, I don’t want to do that, so I used the event Consolidated, and forced the application to exit. For more information about Consolidated event, see this link.

In the next example, I’ll show you how to close all windows in the case the main window is closed. This is a specific scenario, but a non-standard behaviour of the UWP Apps. Most app should leave the secondary views active, and just hide the main view when one is being closed.

 protected override void OnLaunched(LaunchActivatedEventArgs args)
        {
            // Note we're using DisplayRootViewFor (which is view model first)
            // this means we're not creating a root frame and just directly
            // inserting ShellView as the Window.Content

            DisplayRootViewFor();

          //  UIDispatcherHelper.Initialize();
            windowMangerService = IoC.Get();
            windowMangerService.Initialize(ApplicationView.GetForCurrentView().Id);
            ApplicationView.GetForCurrentView().Consolidated += ViewConsolidated;
           
        }

        private void ViewConsolidated(ApplicationView sender, ApplicationViewConsolidatedEventArgs args)
        {
            App.Current.Exit();
        }

Using this service is pretty simple. In the following code, I created a Command and used windows.ShowService to launch the Window with the Type of ViewModel.

 public class MainPageViewModel:Screen
    {
        private IBingSearchService bingService;
        public RelayCommand ShowCarsCommand { get; set; }
        public RelayCommand ShowBuildingsCommand { get; set; }
        public RelayCommand ShowPyramidsCommand { get; set; }

        IWindowManagerService windowService = null;

        public MainPageViewModel()
        {
            bingService = IoC.Get();
            windowService = IoC.Get();
            ShowCarsCommand = new RelayCommand(ShowCarsCommandExecute);
            ShowBuildingsCommand = new RelayCommand(ShowBuildingsCommandExecute);
            ShowPyramidsCommand = new RelayCommand(ShowPyramidsCommandExecute);
        }

        private void ShowPyramidsCommandExecute()
        {
            windowService.ShowAsync(WindowNames.PyramidsWindow, typeof(ViewModels.ImagesViewModel),
                new ImageParameters()
                {
                    Keyword = "pyramids",
                    ViewTitle = "Pyramids View",
                    WindowName= WindowNames.PyramidsWindow
                });
        }

        private void ShowBuildingsCommandExecute()
        {
            windowService.ShowAsync(WindowNames.BuildingsWindow, typeof(ViewModels.ImagesViewModel),
               new ImageParameters()
               {
                   Keyword = "buildings",
                   ViewTitle = "Buildings View",
                   WindowName = WindowNames.BuildingsWindow
               });
        }

        private void ShowCarsCommandExecute()
        {
            windowService.ShowAsync(WindowNames.CarsWindow, typeof(ViewModels.ImagesViewModel),
                new ImageParameters()
                {
                     Keyword="sport cars",
                     ViewTitle="Sport Cars View",
                     WindowName = WindowNames.CarsWindow
                });

        }
        
    }

Conclusion

Managing multiple windows in UWP isn’t easy, as each window has its own dispatcher. When you’re working with threads and want to update some properties from your ViewModel, not using the correct dispatcher for each window could lead to the message: WROG_THREAD_EXCEPTION. With this service, you can access to the dispatcher of the Window.

However, with the approach in this article, one can use the ViewModels in multiple Windows and get access to the correct active window using WindowMangeService. If you’re not sure what the name of your current window is, you can also use Window.Current.Dispatcher.

Since I haven’t found any documentation on this scenario, I wanted to share my experience around this problem. Please download the code to play around with it: https://github.com/RikardoPons/MultiWindowUWP

Thank you!


5001532Ricardo Pons is a lead architect specializing in Microsoft technologies, with a strong focus on Windows, .NET, Microsoft Azure & XAML Applications.  He is a highly-regarded Windows Platform developer in Mexico and Latin America. He likes to create high-quality consumer and enterprise applications, and has built many official applications for global companies for the Windows Phone and Windows Store. A blogger, consultant, speaker and entrepreneur, he was awarded as a Nokia Successful Developer in 2013 and received an Independent Developer Microsoft DX Award in 2014. Follow him on Twitter @RicardoPonsDev.