reactivate exiting window using WindowManager
Asked Answered
S

3

5

I am using WPF with the currently latest and greatest version of Caliburn.Micro (1.4.1). I use IWindowManager.ShowWindow(...) to open an new modeless window:

private void OpenOrReactivateInfoView()
{
    if(this.infoViewModel == null)
    {
        this.infoViewModel = new InfoViewModel();
    }

    this.windowManager.ShowWindow(this.infoViewModel);
}

Instead of opening a new window each time when OpenOrReactivateInfoView() is called, I would like to check whether the window ist still open and if it is, the existing window should just regain focus.

What would we be a good Calibrun.Micro-way to solve this? I sure would like to avoid keeping a reference to the window (or any UIElement for that matter) itself in the viewmodel. Also note that this is a common behavior for a lot of modeless dialogs, so it is preferred solve this in a generic reusable way.

Does Caliburn.Micro already have means for this built in?

Spirituel answered 29/1, 2013 at 12:18 Comment(2)
IsActive property is not enough?Smithers
IsActive does not become false when the associated window loses focus or is minimized and Screen.Activate() does not refocus the associcated window.Spirituel
E
4

A fairly straightforward way to keep track of your windows without actually having to implement IViewAware would be to keep a dictionary of weak references to your ViewModels and Views that go together and then checking if you already have an existing Window or not. Could be implemented either as a decorator to the WindowManager, subclass or extension.

Something as simple as the following should do the trick assuming you don't actually plan on opening enough windows that even the dead WeakReferences would impact performance. If it is going to be long running it shouldn't be that hard to implement some sort of cleanup.

public class MyFancyWindowManager : WindowManager
{
    IDictionary<WeakReference, WeakReference> windows = new Dictionary<WeakReference, WeakReference>();

    public override void ShowWindow(object rootModel, object context = null, IDictionary<string, object> settings = null)
    {
        NavigationWindow navWindow = null;

        if (Application.Current != null && Application.Current.MainWindow != null)
        {
            navWindow = Application.Current.MainWindow as NavigationWindow;
        }

        if (navWindow != null)
        {
            var window = CreatePage(rootModel, context, settings);
            navWindow.Navigate(window);
        }
        else
        {
            var window = GetExistingWindow(rootModel);
            if (window == null)
            {
                window = CreateWindow(rootModel, false, context, settings);
                windows.Add(new WeakReference(rootModel), new WeakReference(window));
                window.Show();
            }
            else
            {
                window.Focus();
            }
        }

    }

    protected virtual Window GetExistingWindow(object model)
    {
        if(!windows.Any(d => d.Key.IsAlive && d.Key.Target == model))
            return null;

        var existingWindow = windows.Single(d => d.Key.Target == model).Value;
        return existingWindow.IsAlive ? existingWindow.Target as Window : null;
    }
}
Exalted answered 8/2, 2013 at 4:45 Comment(0)
D
5

The WindowManager source code always creates a new window, so what you really want to do is only use the WindowManager.ShowWindow method if you actually intend to create a new window.

The first thing you want to do is hold a persistent reference to your view model like this:

private readonly InfoViewModel infoViewModel = new InfoViewModel();
private void OpenOrReactivateInfoView()
{
    this.windowManager.ShowWindow(this.infoViewModel);
}

Then, in your view model, create a method called Focus or whatever you want like this:

public void Focus()
{
    var window = GetView() as Window;
    if (window != null) window.Activate();
}

Then revisit your OpenOrReactivateInfoView() method make a slight adjustment like this:

private void OpenOrReactivateInfoView()
{
    if (!this.infoViewModel.IsActive)
        this.windowManager.ShowWindow(this.infoViewModel);
    else
        this.infoViewModel.Focus();
}

This method worked for me.

Devotion answered 29/1, 2013 at 13:49 Comment(3)
This is what I have come up with in the meantime now too. What I don't really like about it is that my ViewModel must be an IViewAware and needs to hold a reference to the view. Also I would like to make this functionality generic and reusable.Spirituel
I completely agree. It's not the most elegant solution, for sure. I think the crux of the problem is in Caliburn.Micro's WindowManager class. The most elegant and re-usable solution here would be to create your own WindowManager class to replace Caliburn.Micro's default implementation.Devotion
Or adapt/intercept the WindowManager.Spirituel
E
4

A fairly straightforward way to keep track of your windows without actually having to implement IViewAware would be to keep a dictionary of weak references to your ViewModels and Views that go together and then checking if you already have an existing Window or not. Could be implemented either as a decorator to the WindowManager, subclass or extension.

Something as simple as the following should do the trick assuming you don't actually plan on opening enough windows that even the dead WeakReferences would impact performance. If it is going to be long running it shouldn't be that hard to implement some sort of cleanup.

public class MyFancyWindowManager : WindowManager
{
    IDictionary<WeakReference, WeakReference> windows = new Dictionary<WeakReference, WeakReference>();

    public override void ShowWindow(object rootModel, object context = null, IDictionary<string, object> settings = null)
    {
        NavigationWindow navWindow = null;

        if (Application.Current != null && Application.Current.MainWindow != null)
        {
            navWindow = Application.Current.MainWindow as NavigationWindow;
        }

        if (navWindow != null)
        {
            var window = CreatePage(rootModel, context, settings);
            navWindow.Navigate(window);
        }
        else
        {
            var window = GetExistingWindow(rootModel);
            if (window == null)
            {
                window = CreateWindow(rootModel, false, context, settings);
                windows.Add(new WeakReference(rootModel), new WeakReference(window));
                window.Show();
            }
            else
            {
                window.Focus();
            }
        }

    }

    protected virtual Window GetExistingWindow(object model)
    {
        if(!windows.Any(d => d.Key.IsAlive && d.Key.Target == model))
            return null;

        var existingWindow = windows.Single(d => d.Key.Target == model).Value;
        return existingWindow.IsAlive ? existingWindow.Target as Window : null;
    }
}
Exalted answered 8/2, 2013 at 4:45 Comment(0)
S
1

I have come up with this extension method. It works but I am not particulary happy with it, it is still somewhat hackish.

It is clearly a designsmell that this extension has to make so many assumption about the model (do you see also those nasty exceptions?).

using System;
using System.Collections.Generic;
using Caliburn.Micro;

public static class WindowManagerExtensions
{
    /// <summary>
    /// Shows a non-modal window for the specified model or refocuses the exsiting window.  
    /// </summary>
    /// <remarks>
    /// If the model is already associated with a view and the view is a window that window will just be refocused
    /// and the parameter <paramref name="settings"/> is ignored.
    /// </remarks>
    public static void FocusOrShowWindow(this IWindowManager windowManager,
                                         object model,
                                         object context = null,
                                         IDictionary<string, object> settings = null)
    {
        var activate = model as IActivate;
        if (activate == null)
        {
            throw new ArgumentException(
                string.Format("An instance of type {0} is required", typeof (IActivate)), "model");
        }

        var viewAware = model as IViewAware;
        if (viewAware == null)
        {
            throw new ArgumentException(
                string.Format("An instance of type {0} is required", typeof (IViewAware)), "model");
        }

        if (!activate.IsActive)
        {
            windowManager.ShowWindow(model, context, settings);
            return;
        }

        var view = viewAware.GetView(context);
        if (view == null)
        {
            throw new InvalidOperationException("View aware that is active must have an attached view.");
        }

        var focus = view.GetType().GetMethod("Focus");
        if (focus == null)
        {
            throw new InvalidOperationException("Attached view requires to have a Focus method");
        }

        focus.Invoke(view, null);
    }
}
Spirituel answered 29/1, 2013 at 15:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.