[英]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.