[英]Resolving windows in Structure Map or how to manage multiple windows in WPF MVVM?
我一直在阅读Mark Seeman关于.NET中依赖注入的书,我正在努力在WPF应用程序中配置组合根。
我的容器将在应用程序启动方法中注册:
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var container = new Container();
container.Configure(r =>
{
r.For<IAccountServices>().Use<AccountServicesProxy>();
r.For<MainWindow>().Use<MainWindow>();
});
}
这有意义,因为应用程序启动代表我的组合根。
我的应用程序中的WPF窗口基于视图模型。 视图模型使用构造函数注入。 例如,我可以通过注入IAccountServices
实现来组成视图模型。
在创建主窗口时,我可以在OnStartup方法中执行以下操作:
var mainWindow = container.GetInstance<MainWindow>();
mainWindow.Show();
一旦我进入主窗口,我可能想要打开另一个窗口。 到目前为止,我已经能够提出一种方法,即创建一个窗口工厂并请求窗口工厂解析窗口的实例。 我必须确保窗口工厂在每个可能需要打开新窗口的视图模型中都可用。 在我看来,这与在我的应用程序中传递IoC容器一样糟糕(服务定位器反模式会浮现在脑海中)。
这种方法对你来说是否正确? 我的直觉告诉我这是错的,但我还没有想出一个更好的方法来实现这个目标。
我认为在实现行为模式之前,例如Mediator
等,需要决定一个通用模式以便于应用程序结构。 为此目的,即为创建独立窗口,非常适合Abstract factory
模式。
可以使用诸如IDialogService
方法在ViewModel
侧实现窗口的创建。 但我认为这个任务应该在View
侧面实现,因为Window
对象是指View
而不是ViewModel
。 因此,您必须创建MVVM样式架构,它允许使用设计模式创建独立窗口。
我创建了一个项目,其中一个Abstract factory
使用附加的行为在View
的一侧创建一个Window。 Abstract factory
还实现了Singleton模式,以创建全局访问点并确保新构造对象的唯一性。 附加行为隐式实现了模式Decorator,它是在XAML一侧使用的抽象工厂的包装器。 对于Abstract factory
,不引用位于ViewModel
对象使用代理模式,该模式是具有不带DataType的DataTemplate的ContentControl。 还使用Command
模式在对象之间进行独立操作。 因此,该项目使用以下模式:
项目结构如下所示:
在附加的行为中附加了依赖属性Name
,它以新窗口的名称传输。 为他注册了PropertyChangedEvent
,这是一个调用Make方法的抽象工厂:
private static void IsFactoryStart(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var window = sender as Window;
if (window == null)
{
return;
}
if (e.NewValue is String && String.IsNullOrEmpty((string)e.NewValue) == false)
{
_typeWindow = (string)e.NewValue;
if (_typeWindow != null)
{
var newWindow = WindowFactory.Instance.Make(_typeWindow);
newWindow.Show();
}
}
}
WindowFactory
和Singleton模式如下所示:
public class WindowFactory : IWindowFactory
{
#region WindowFactory Singleton Instance
private static WindowFactory _instance = null;
private static readonly object padlock = new object();
public static WindowFactory Instance
{
get
{
lock (padlock)
{
if (_instance == null)
{
_instance = new WindowFactory();
}
return _instance;
}
}
}
#endregion
public Window Make(string TypeWindow)
{
if (TypeWindow.Equals("WindowOneViewProxy"))
{
var windowOne = new Window();
windowOne.Width = 450;
windowOne.Height = 250;
windowOne.WindowStartupLocation = WindowStartupLocation.CenterScreen;
windowOne.Title = TypeWindow;
windowOne.ContentTemplate = Application.Current.Resources[TypeWindow] as DataTemplate;
return windowOne;
}
else if (TypeWindow.Equals("WindowTwoViewProxy"))
{
var windowTwo = new Window();
windowTwo.Width = 500;
windowTwo.Height = 200;
windowTwo.WindowStartupLocation = WindowStartupLocation.CenterScreen;
windowTwo.Title = TypeWindow;
windowTwo.ContentTemplate = Application.Current.Resources[TypeWindow] as DataTemplate;
return windowTwo;
}
else if (TypeWindow.Equals("WindowThreeViewProxy"))
{
var windowThree = new Window();
windowThree.Width = 400;
windowThree.Height = 140;
windowThree.WindowStartupLocation = WindowStartupLocation.CenterScreen;
windowThree.Title = TypeWindow;
windowThree.ContentTemplate = Application.Current.Resources[TypeWindow] as DataTemplate;
return windowThree;
}
else
throw new Exception("Factory can not create a: {0}" + TypeWindow);
}
}
对于属性Window.ContentTemplate
从资源中设置DataTemplate。 ContentTemplate
负责可视化表示,为了从ViewModel绑定属性,需要将对象设置为Content。 但在这种情况下, Abstract factory
引用将为ViewModel,并避免它们并使用代理模式如下:
WindowOneProxyView
<DataTemplate x:Key="WindowOneViewProxy">
<ContentControl ContentTemplate="{StaticResource WindowOneViewRealObject}">
<ViewModels:WindowOneViewModel />
</ContentControl>
</DataTemplate>
WindowOneViewRealObject
<DataTemplate x:Key="WindowOneViewRealObject" DataType="{x:Type ViewModels:WindowOneViewModel}">
<Grid>
<Label Content="{Binding Path=WindowOneModel.TextContent}"
HorizontalAlignment="Center"
VerticalAlignment="Top"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Background="Beige" />
<Button Content="One command"
Width="100"
Height="30"
HorizontalAlignment="Center"
Command="{Binding OneCommand}" />
</Grid>
</DataTemplate>
在DataTemplate
代理不是指定DataType,而是在真实对象中。
在MainViewModel
有命令来简单地设置窗口名称,它将为附加的行为提供输入:
MainModel
public class MainModel : NotificationObject
{
#region TypeName
private string _typeName = null;
public string TypeName
{
get
{
return _typeName;
}
set
{
_typeName = value;
NotifyPropertyChanged("TypeName");
}
}
#endregion
}
MainViewModel
public class MainViewModel
{
#region MainModel
private MainModel _mainModel = null;
public MainModel MainModel
{
get
{
return _mainModel;
}
set
{
_mainModel = value;
}
}
#endregion
#region ShowWindowOneCommand
private ICommand _showWindowOneCommand = null;
public ICommand ShowWindowOneCommand
{
get
{
if (_showWindowOneCommand == null)
{
_showWindowOneCommand = new RelayCommand(param => this.ShowWindowOne(), null);
}
return _showWindowOneCommand;
}
}
private void ShowWindowOne()
{
MainModel.TypeName = "WindowOneViewProxy";
}
#endregion
#region ShowWindowTwoCommand
private ICommand _showWindowTwoCommand = null;
public ICommand ShowWindowTwoCommand
{
get
{
if (_showWindowTwoCommand == null)
{
_showWindowTwoCommand = new RelayCommand(param => this.ShowWindowTwo(), null);
}
return _showWindowTwoCommand;
}
}
private void ShowWindowTwo()
{
MainModel.TypeName = "WindowTwoViewProxy";
}
#endregion
#region ShowWindowThreeCommand
private ICommand _showWindowThreeCommand = null;
public ICommand ShowWindowThreeCommand
{
get
{
if (_showWindowThreeCommand == null)
{
_showWindowThreeCommand = new RelayCommand(param => this.ShowWindowThree(), null);
}
return _showWindowThreeCommand;
}
}
private void ShowWindowThree()
{
MainModel.TypeName = "WindowThreeViewProxy";
}
#endregion
public MainViewModel()
{
MainModel = new MainModel();
}
}
MainWindow
看起来像:
<Window x:Class="WindowFactoryNamespace.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:this="clr-namespace:WindowFactoryNamespace.ViewModels"
xmlns:AttachedBehaviors="clr-namespace:WindowFactoryNamespace.AttachedBehaviors"
AttachedBehaviors:WindowFactoryBehavior.Name="{Binding Path=MainModel.TypeName}"
WindowStartupLocation="CenterScreen"
Title="MainWindow" Height="300" Width="300">
<Window.DataContext>
<this:MainViewModel />
</Window.DataContext>
<WrapPanel>
<Button Content="WindowOne"
Margin="10"
Command="{Binding ShowWindowOneCommand}" />
<Button Content="WindowTwo"
Margin="10"
Command="{Binding ShowWindowTwoCommand}" />
<Button Content="WindowThree"
Margin="10"
Command="{Binding ShowWindowThreeCommand}" />
</WrapPanel>
</Window>
第一个窗口的Test View-ViewModel
看起来像这样(它们几乎完全相同):
WindowOneModel
public class WindowOneModel : NotificationObject
{
#region TextContent
private string _textContent = "Text content for WindowOneView";
public string TextContent
{
get
{
return _textContent;
}
set
{
_textContent = value;
NotifyPropertyChanged("TextContent");
}
}
#endregion
}
WindowOneViewModel
public class WindowOneViewModel
{
#region WindowOneModel
private WindowOneModel _windowOneModel = null;
public WindowOneModel WindowOneModel
{
get
{
return _windowOneModel;
}
set
{
_windowOneModel = value;
}
}
#endregion
#region OneCommand
private ICommand _oneCommand = null;
public ICommand OneCommand
{
get
{
if (_oneCommand == null)
{
_oneCommand = new RelayCommand(param => this.One(), null);
}
return _oneCommand;
}
}
private void One()
{
WindowOneModel.TextContent = "Command One change TextContent";
}
#endregion
public WindowOneViewModel()
{
WindowOneModel = new WindowOneModel();
}
}
该项目可在此
link
。
Output
MainWindow
WindowOne
WindowTwo
WindowThree
恕我直言,为了MVVM纯度,没有必要使解决方案复杂化。 您可能会冒后续开发人员不理解您的优雅解决方案并将其打破。 事实上,由于复杂性,“纯”实现往往不具有可读性,因此很有可能。
恕我直言,任何以抽象方式永久解决问题的解决方案,只需最少的代码开销和简单的使用,就比每次使用解决方案时花费大量开销更好,即使达到“纯度”(它不会用于任何目的) 。 在应用程序中显示对话框的问题必须解决一次,并且将来应该很容易使用它。
组合视图模型非常精细,通过允许视图模型在没有戏剧性的情况下进行交互,可以使生活更轻松
可以创建一个对话框服务,它将充当应用程序中所有对话框需求的包装器。 您可以将对话框服务和需要在窗口中显示的子视图模型注入父视图模型。 当您需要显示窗口时,请求Dialog服务执行此操作,并将视图模型实例和视图名称传递给它。
注意:代码未被编译或测试
public class DialogService : IDialogService
{
IEventAggregator _eventAggregator;
bool _fatalError;
//Provides a wrapper function which will connect your view and view model and open a
//dialog
public Window ShowCustomDialog<TViewModel>(string name, TViewModel viewModel, bool
modal, double left, double top, Action<bool?> OnClose, int width, int height)
{
if (_fatalError == true)
{
return null;
}
Window view = new Window(name);
if (viewModel != null)
{
view.DataContext = viewModel;
}
if (left != -1.0 && top != -1.0)
{
view.WindowStartupLocation = WindowStartupLocation.Manual;
view.Left = left;
view.Top = top;
}
else
{
view.WindowStartupLocation = WindowStartupLocation.CenterScreen;
}
if (width != -1 && height != -1)
{
view.Width = width;
view.Height = height;
}
view.Closed += (o, e) =>
{
_eventAggregator.GetEvent<NotifyDialogAction>().Publish(false);
if (OnClose != null)
{
OnClose(e.DialogResult);
}
};
view.Loaded += (o, e) =>
{
_eventAggregator.GetEvent<NotifyDialogAction>().Publish(true);
Window window = o as Window;
if (window != null)
{
double dialogWidth = window.ActualWidth;
double screenWidth =
Application.Current.RootVisual.RenderSize.Width;
double dialogLeft = window.Left;
if (dialogLeft + dialogWidth > screenWidth)
{
window.Left = screenWidth - dialogWidth;
}
double dialogHeight = window.ActualHeight;
double screenHeight =
Application.Current.RootVisual.RenderSize.Height;
double dialogTop = window.Top;
if (dialogTop + dialogHeight > screenHeight)
{
window.Top = screenHeight - dialogHeight;
}
}
};
if (modal)
{
view.ShowDialog();
}
else
{
view.Show();
}
return view;
}
//Add more functions. For example to pop up a message box etc.
}
用法
public class ComposedVM
{
public ViewModelA objA{get;set;}
public ViewModelB objB{get;set;}
IDialogService dialogService{get;set;}
public ComposedVM(ViewModelA a, ViewModelB b, IDialogService dlg )
{
objA = a;
objB = b;
dialogService = dlg
}
public void OnShowWindowACommand()
{
dialogService .ShowCustomDialog<object>(
DialogNames.ViewA/*view name constant*/, objA, true, -1.0, -1.0,
result =>
{
if (result == true)
{
dialogService.ShowMessageDialog(ApplicationStrings.SuccessFulOperation);
}
});
}
}
可以在模块之间使用基于事件/消息的通信。 将它用于模块中的相关视图模型是一种矫枉过正的恕我直言。
在99%的情况下,通过构造函数推送容器实例是一个坏主意,因为容器是服务定位器。 这种方法的主要缺点是:
有许多方法可以用MVVM方式创建窗口:
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.