When i use IoC container to control windows in WPF MVVM, Open Window workes, but Close Window doesn't work

368 views Asked by At

I want to create a Interface to control all windows. I used Microsoft.Extensions.DependencyInjection .

Interface IWindowManager and class WindowManager as:

public interface IWindowManager
    {
        void OpenWindow<TWindow>() where TWindow : Window;
        void CloseWindow<TWindow>() where TWindow : Window;
    }
public class WindowManager : IWindowManager
    {
        private readonly IServiceProvider _serviceProvider;

        public WindowManager(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        public void OpenWindow<TWindow>() where TWindow : Window
        {
            var window = _serviceProvider.GetRequiredService<TWindow>();
            window.Show();
        }

        public void CloseWindow<TWindow>() where TWindow : Window
        {
            var window = _serviceProvider.GetRequiredService<TWindow>();
            window.Close();
        }
    }

In App.xaml.cs, i create IoC container, it's the same as the code of Microsoft Commuinty document https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/ioc

public partial class App : Application
    {
        public App() 
        {
            Services = ConfigureServices();
        }

        public new static App Current => (App)Application.Current;

        public IServiceProvider Services { get; }

        private static IServiceProvider ConfigureServices()
        {
            var services = new ServiceCollection();
            services.AddSingleton<IWindowManager, WindowManager>();

            services.AddTransient<MainWindowViewModel>();
            services.AddTransient<MainWindow>(
                sp => new MainWindow
                { DataContext = sp.GetService<MainWindowViewModel>() });

            services.AddTransient<TaskWindowViewModel>();
            services.AddTransient<TaskWindow>(
                sp => new TaskWindow
                { DataContext = sp.GetService<TaskWindowViewModel>() });

            return services.BuildServiceProvider();
        }

        private void Application_Startup(object sender, StartupEventArgs e)
        {
            var MW = Services.GetService<MainWindow>();
            MW!.Show();
        }
    }

In MainWindowViewModel.cs, i create two RelayCommand method by CommunityToolkit.Mvvm, and binding with button in Xaml:

 public partial class MainWindowViewModel : ObservableObject
    {
        private readonly IWindowManager _windowManager;

        #region Windows Manager
        [RelayCommand]
        private void OpenTaskWindow()
        {
            _windowManager.OpenWindow<TaskWindow>();
        }

        [RelayCommand]
        private void CloseMainWindow()
        {
            _windowManager.CloseWindow<MainWindow>();
        }
        #endregion

        public MainWindowViewModel( IWindowManager windowManager)
        {
            _windowManager = windowManager;
        }
    }

I tried the method of OpenTaskWindow workes, but CloseMainWindow does't work. I don't know what's wrong.Please help me, thank you.

4

There are 4 answers

1
Cherish On BEST ANSWER

I resolved it. Add a dictionary viewModelWindowMapping to save types of windows and viewModels.

    private Dictionary<object, Window> windowList = new();

    private Dictionary<Type, Type> viewModelWindowMapping = new()
    {
        { typeof(MainWindowViewModel),typeof(MainWindow)},
        { typeof(TaskWindowViewModel),typeof(TaskWindow)}
    };

    public WindowManager(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public void OpenWindow(object viewModel)
    {
        if (viewModel == null)
        {
            throw new ArgumentNullException(nameof(viewModel));
        }

        if (windowList.ContainsKey(viewModel))
        {
            return;
        }

        Window window = GetWindowForViewModel(viewModel);

        windowList[viewModel] = window;

        window.Show();
    }

    public void CloseWindow(object viewModel)
    {
        if (viewModel == null)
        {
            throw new ArgumentNullException(nameof(viewModel));
        }

        if (!windowList.ContainsKey(viewModel))
        {
            return;
        }

        Window window = windowList[viewModel];

        window.Close();

        windowList.Remove(viewModel);
    }

    private Window GetWindowForViewModel(object viewModel)
    {
        var windowType = GetWindowTypeForViewModel(viewModel.GetType());
        return (Window)_serviceProvider.GetRequiredService(windowType);
    }

    private Type GetWindowTypeForViewModel(Type viewModelType)
    {
        if (viewModelWindowMapping.TryGetValue(viewModelType, out var windowType))
        {
            return windowType;
        }
        throw new Exception($"No window type is mapped for the view model type {viewModelType.FullName}");
    }

Like this, when i want to add another window. I just need add one mapping in viewModelWindowMapping

1
Sir Rufo On

When you want to open a window with the WindowManager you did

  1. You get a new window instance from DI-Container
    var window = _serviceProvider.GetRequiredService<TWindow>();
    
  2. You show that new window instance
    window.Show();
    

And when you want to close a window with the WindowManager you did

  1. You get a new window instance from DI-Container
    var window = _serviceProvider.GetRequiredService<TWindow>();
    
  2. You close that new window instance
    window.Close();
    

And that is what is wrong: You always create a new instance - because the window is registered as transient - to close it when you want to close a window that is already shown.

3
mm8 On

You should close a window instance that has previously been opened.

You could for example handle this in your WindowManager class by keeping track of the currently opened windows in a collection, e.g.:

public class WindowManager : IWindowManager
{
    private readonly IServiceProvider _serviceProvider;
    private readonly List<Window> _openWindows = List<Window>();

    public WindowManager(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public void OpenWindow<TWindow>() where TWindow : Window
    {
        var window = _serviceProvider.GetService<TWindow>();
        if (window != null)
        {
            _openWindows.Add(window);
            window.Show();
        }
    }

    public void CloseWindow<TWindow>() where TWindow : Window
    {
        var window = _openWindows.OfType<TWindow>().FirstOrDefault();
        if (window != null)
        {
            window.Close();
            _openWindows.Remove(window);
        }
    }
}
1
Cherish On

I treid to rewrite CloseWindow method in MainWindow class.

public partial class MainWindow : Window, IWindowManager
{
    public MainWindow()
    {
        InitializeComponent();
    }

    public void CloseWindow() => Close();
}

And Button Clicked methond in MainWindowViewModel class.

    [RelayCommand]
    private void CloseMainWindow(IWindowManager windowManager)
    {
        windowManager.CloseWindow();
    }

It works, but not what i want.That means i will overrid CloseWindow method in every View.And my intention is to control the Opening or Closing of all windows through an interface.