MVVM Opening windows 同时保持 View 和 View Model 解耦
MVVM Opening windows while keeping View and View Model decoupled
我是第一次尝试使用 MVVM 模式,但我很难打开视图,同时又要让它们与视图模型分离。我正在使用 DialogService
class(下面的 IDialog.cs),它是 YouTube 上 MVVM 教程的一部分。 DialogService
只要从具有 DialogService
.
实例的 MainWindow 访问它就可以正常工作
问题是我需要从没有 DialogService
实例的 TradeManagerViewModel
打开多个 TradeView
。我无法创建 DialogService
的另一个实例,因为我需要为我创建的每个实例注册所有 View/ViewModel 映射。我无法使用我的 MainWindowViewModel
中的 DialogService
实例,因为我的 TradeMangerViewModel
没有对我的 MainWindowViewModel
实例的引用。在主 window 视图模型中,我无法将 public readonly IDialogService dialogService;
设为静态,因为这样我就无法分配在 MainWindowViewModel
构造函数中传递的 dialogService
参数。
我能想到的唯一其他方法是创建一个单独的单例 class 来保存 DialogService
的实例,这样同一个实例就可以从两个视图模型(以及我将来的视图模型)访问还没有写)。但我也读过很多关于单例 classes 的不同观点,其中大多数建议你永远不需要使用它们。那么我是否发现了该意见的例外情况?或者我 can/should 有其他方法吗?
App.xaml.cs(此处的更改也来自 YouTube 视频)
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
IDialogService dialogService = new DialogService(MainWindow);
dialogService.Register<TradeViewModel, TradeView>();
dialogService.Register<TradeManagerViewModel, TradeManager>();
var viewModel = new MainWindowViewModel(dialogService);
base.OnStartup(e);
}
}
IDialog.cs
/// <summary>
/// Allows Windows/Dialogs to be opened and closed without coupling the View to the ViewModel
/// </summary>
public interface IDialog
{
object DataContext { get; set; }
bool? DialogResult { get; set; }
Window Owner { get; set; }
void Close();
bool? ShowDialog();
}
/// <summary>
/// Registers a dictionary of View Models to the the correct Views allowing the correct View to be displayed when an instance of a View Model is instantiated
/// </summary>
public interface IDialogService
{
void Register<TViewModel, TView>() where TViewModel : IDialogRequestClose
where TView : IDialog;
bool? ShowDailog<TViewModel>(TViewModel viewModel) where TViewModel : IDialogRequestClose;
}
/// <summary>
/// Creates an Event Handler which handles close requests for the dialog
/// </summary>
public interface IDialogRequestClose
{
event EventHandler<DialogCloseRequestedEventArgs> CloseRequested;
}
public class DialogCloseRequestedEventArgs : EventArgs
{
public DialogCloseRequestedEventArgs(bool? dialogResult)
{
DialogResult = dialogResult;
}
public bool? DialogResult { get; }
}
public class DialogService : IDialogService
{
private readonly Window owner;
/// <summary>
/// Initialises the DialogService and sets its owner
/// </summary>
/// <param name="owner">The Window which will own the DialogService. The main window of the application will probably be the best owner.</param>
public DialogService(Window owner)
{
this.owner = owner;
Mappings = new Dictionary<Type, Type>();
}
public IDictionary<Type, Type> Mappings { get; } //Used to store which type of View should be used with each ViewModel
/// <summary>
/// Register which View should be used with a ViewModel
/// </summary>
/// <typeparam name="TViewModel">Type of ViewModel</typeparam>
/// <typeparam name="TView">Type of View</typeparam>
public void Register<TViewModel, TView>()
where TViewModel : IDialogRequestClose
where TView : IDialog
{
if (Mappings.ContainsKey(typeof(TViewModel))) //If a mapping already exists for this type of ViewModel
{
throw new ArgumentException($"Type {typeof(TViewModel)} is already mapped to type {typeof(TView)}");
}
Mappings.Add(typeof(TViewModel), typeof(TView)); //Otherwise create a new mapping
}
/// <summary>
/// Shows the correct View for the given ViewModel and subscribes to the close request handler
/// </summary>
/// <typeparam name="TViewModel"></typeparam>
/// <param name="viewModel">ViewModel which you want to open the mapped View for</param>
/// <returns>Returns bool dialog result</returns>
public bool? ShowDailog<TViewModel>(TViewModel viewModel) where TViewModel : IDialogRequestClose
{
Type viewType = Mappings[typeof(TViewModel)]; //Get the type of View associated with this type of ViewModel from the Mappings Dictionary
IDialog dialog = (IDialog)Activator.CreateInstance(viewType); //Create an instance of the mapped view
EventHandler<DialogCloseRequestedEventArgs> handler = null;
// When the handler is called, unsubscribe from the event as we no longer need to listen to it once the View has been closed
handler = (sender, e) =>
{
viewModel.CloseRequested -= handler;
if (e.DialogResult.HasValue)
{
dialog.DialogResult = e.DialogResult;
} else
{
dialog.Close();
}
};
//Subscribe to the CloseRequested event
viewModel.CloseRequested += handler;
dialog.DataContext = viewModel;
dialog.Owner = owner;
return dialog.ShowDialog();
}
}
MainWindowViewModel.cs
internal class MainWindowViewModel
{
public readonly IDialogService dialogService;
public MainWindowViewModel(IDialogService dialogService)
{
this.dialogService = dialogService;
//Load settings etc. removed.
//This works here, but dialogService isn't accessible in TradeManagerViewModel:
var tradeManagerViewModel = new TradeManagerViewModel(filePath);
bool? result = this.dialogService.ShowDialog(tradeManagerViewModel);
}
}
解耦的解决方案,一般来说,就是利用Control的DependencyInjection/Inversion。您可以使用任何 DI 容器(如 Unity)。
此外,您可以使用像 Prism 这样的 MVVM 框架,它可以帮助您创建松散耦合且可维护的整个应用程序。
正如其他人所建议的那样,您会受益于 IoC 容器,但我认为您不应该从 Prism 开始。从小处着手,使用 MVVM Light 中的 IoC 容器,有大量示例向您展示如何使用该库编写应用程序。
您还可以查看 MVVM Dialogs 的示例,其中有许多示例在 IoC 容器中设置对话服务。
But I've also read lots of different opinions about singleton classes and most of them suggesting that you shouldn't ever really need to use them.
这是完全错误的。事实上,单例对于使彼此不认识的实例能够进行通信非常有帮助。我会使用像 only make those classes a singleton that need to be one
这样的弱化语句,但完全没有理由完全避免单例。
我是第一次尝试使用 MVVM 模式,但我很难打开视图,同时又要让它们与视图模型分离。我正在使用 DialogService
class(下面的 IDialog.cs),它是 YouTube 上 MVVM 教程的一部分。 DialogService
只要从具有 DialogService
.
问题是我需要从没有 DialogService
实例的 TradeManagerViewModel
打开多个 TradeView
。我无法创建 DialogService
的另一个实例,因为我需要为我创建的每个实例注册所有 View/ViewModel 映射。我无法使用我的 MainWindowViewModel
中的 DialogService
实例,因为我的 TradeMangerViewModel
没有对我的 MainWindowViewModel
实例的引用。在主 window 视图模型中,我无法将 public readonly IDialogService dialogService;
设为静态,因为这样我就无法分配在 MainWindowViewModel
构造函数中传递的 dialogService
参数。
我能想到的唯一其他方法是创建一个单独的单例 class 来保存 DialogService
的实例,这样同一个实例就可以从两个视图模型(以及我将来的视图模型)访问还没有写)。但我也读过很多关于单例 classes 的不同观点,其中大多数建议你永远不需要使用它们。那么我是否发现了该意见的例外情况?或者我 can/should 有其他方法吗?
App.xaml.cs(此处的更改也来自 YouTube 视频)
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
IDialogService dialogService = new DialogService(MainWindow);
dialogService.Register<TradeViewModel, TradeView>();
dialogService.Register<TradeManagerViewModel, TradeManager>();
var viewModel = new MainWindowViewModel(dialogService);
base.OnStartup(e);
}
}
IDialog.cs
/// <summary>
/// Allows Windows/Dialogs to be opened and closed without coupling the View to the ViewModel
/// </summary>
public interface IDialog
{
object DataContext { get; set; }
bool? DialogResult { get; set; }
Window Owner { get; set; }
void Close();
bool? ShowDialog();
}
/// <summary>
/// Registers a dictionary of View Models to the the correct Views allowing the correct View to be displayed when an instance of a View Model is instantiated
/// </summary>
public interface IDialogService
{
void Register<TViewModel, TView>() where TViewModel : IDialogRequestClose
where TView : IDialog;
bool? ShowDailog<TViewModel>(TViewModel viewModel) where TViewModel : IDialogRequestClose;
}
/// <summary>
/// Creates an Event Handler which handles close requests for the dialog
/// </summary>
public interface IDialogRequestClose
{
event EventHandler<DialogCloseRequestedEventArgs> CloseRequested;
}
public class DialogCloseRequestedEventArgs : EventArgs
{
public DialogCloseRequestedEventArgs(bool? dialogResult)
{
DialogResult = dialogResult;
}
public bool? DialogResult { get; }
}
public class DialogService : IDialogService
{
private readonly Window owner;
/// <summary>
/// Initialises the DialogService and sets its owner
/// </summary>
/// <param name="owner">The Window which will own the DialogService. The main window of the application will probably be the best owner.</param>
public DialogService(Window owner)
{
this.owner = owner;
Mappings = new Dictionary<Type, Type>();
}
public IDictionary<Type, Type> Mappings { get; } //Used to store which type of View should be used with each ViewModel
/// <summary>
/// Register which View should be used with a ViewModel
/// </summary>
/// <typeparam name="TViewModel">Type of ViewModel</typeparam>
/// <typeparam name="TView">Type of View</typeparam>
public void Register<TViewModel, TView>()
where TViewModel : IDialogRequestClose
where TView : IDialog
{
if (Mappings.ContainsKey(typeof(TViewModel))) //If a mapping already exists for this type of ViewModel
{
throw new ArgumentException($"Type {typeof(TViewModel)} is already mapped to type {typeof(TView)}");
}
Mappings.Add(typeof(TViewModel), typeof(TView)); //Otherwise create a new mapping
}
/// <summary>
/// Shows the correct View for the given ViewModel and subscribes to the close request handler
/// </summary>
/// <typeparam name="TViewModel"></typeparam>
/// <param name="viewModel">ViewModel which you want to open the mapped View for</param>
/// <returns>Returns bool dialog result</returns>
public bool? ShowDailog<TViewModel>(TViewModel viewModel) where TViewModel : IDialogRequestClose
{
Type viewType = Mappings[typeof(TViewModel)]; //Get the type of View associated with this type of ViewModel from the Mappings Dictionary
IDialog dialog = (IDialog)Activator.CreateInstance(viewType); //Create an instance of the mapped view
EventHandler<DialogCloseRequestedEventArgs> handler = null;
// When the handler is called, unsubscribe from the event as we no longer need to listen to it once the View has been closed
handler = (sender, e) =>
{
viewModel.CloseRequested -= handler;
if (e.DialogResult.HasValue)
{
dialog.DialogResult = e.DialogResult;
} else
{
dialog.Close();
}
};
//Subscribe to the CloseRequested event
viewModel.CloseRequested += handler;
dialog.DataContext = viewModel;
dialog.Owner = owner;
return dialog.ShowDialog();
}
}
MainWindowViewModel.cs
internal class MainWindowViewModel
{
public readonly IDialogService dialogService;
public MainWindowViewModel(IDialogService dialogService)
{
this.dialogService = dialogService;
//Load settings etc. removed.
//This works here, but dialogService isn't accessible in TradeManagerViewModel:
var tradeManagerViewModel = new TradeManagerViewModel(filePath);
bool? result = this.dialogService.ShowDialog(tradeManagerViewModel);
}
}
解耦的解决方案,一般来说,就是利用Control的DependencyInjection/Inversion。您可以使用任何 DI 容器(如 Unity)。
此外,您可以使用像 Prism 这样的 MVVM 框架,它可以帮助您创建松散耦合且可维护的整个应用程序。
正如其他人所建议的那样,您会受益于 IoC 容器,但我认为您不应该从 Prism 开始。从小处着手,使用 MVVM Light 中的 IoC 容器,有大量示例向您展示如何使用该库编写应用程序。
您还可以查看 MVVM Dialogs 的示例,其中有许多示例在 IoC 容器中设置对话服务。
But I've also read lots of different opinions about singleton classes and most of them suggesting that you shouldn't ever really need to use them.
这是完全错误的。事实上,单例对于使彼此不认识的实例能够进行通信非常有帮助。我会使用像 only make those classes a singleton that need to be one
这样的弱化语句,但完全没有理由完全避免单例。