ViewModel-first方法對Stylet的架構至關重要,但如果你以傳統的View-first方式學習MVVM,那么這種方法就不直觀了。
希望本文能把一切都說清楚。
視圖優先方法
讓我們從定義視圖優先方法開始。MVVM 聲明 ViewModel 應該對 View 一無所知,反過來說View應該知道 ViewModel。將View與ViewModel綁定在一起的最簡單方式是將ViewModel放置在View的Codebehind里,類似下面的代碼:
public partial class MyView : Window
{
public MyView()
{
InitializeComponent();
this.DataContext = new MyViewModel();
}
}
當然視圖還可以創建和擁有其他視圖,可以將多個視圖構成視圖樹,所有這些都還好。
但是像下面這樣的情況,
<!-- This is a window which contains a top bar and another page -->
<Window x:Class="MyNamespace.ShellView" ....>
<StackPanel>
<my:TopBarView/>
<Frame x:Name="navigationFrame"/>
</StackPanel>
</Window>
這里的TopBarView有其ViewModel,TopBarViewModel。
假定TopBarView里有一個字段的數據想要去更新,比如當前頁面的標題。現在,ShellViewModel知曉哪一個Page是當前頁面,但是TopBarViewModel不知道。怎么辦,只好在TopBarView中暴露一個依賴屬性,然后緘定到ShellViewModel,如下所示:
<Window x:Class="MyNamespace.ShellView" .... x:Name="rootObject">
<StackPanel>
<my:TopBarView CurrentPageTitle="{Binding CurrentPageTitle, ElementName=rootObject}"/>
<Frame x:Name="navigationFrame"/>
</StackPanel>
</Window>
這真的不夠優雅。
另一個主要問題是顯示窗口和對話框。在傳統的MVVM中,這有點痛苦。一種選擇是從 ViewModel 內部實例化和顯示 View(using Show()或 ShowDialog()),這使其或至少其中的一部分無法測試)。更好的選擇是在視圖的codebehind中實例化,然后在那里顯示。這意味着您需要建立告訴View顯示此對話框的方法,以及將對話框的結果返回到 ViewModel 的方法。
實際上,設置上述Frame內容需要實例化視圖以放入其中。這具有相同的困境 - 要么 ViewModel 實例化它(使其不可測試),要么在視圖實例化它(導致通信痛苦)。
無論哪種方式,這種方法都不太優雅。
ViewModel優先的實踐
ViewModel優先的模式使得ViewModel與View相互之間獨立存在,實現了完美的分離。取而代之的是采用第三方的服務來建立View與ViewModel之間的關系,配置其相應的DataContext。
默認的實現是使用命名約定來建立聯系,對於一個給定的ViewModel,將其變量名中的“ViewModel”替換為“View”即可。更多細節參見ViewManager。
這使得ViewModel可以由其他ViewModel創建,也允許組合ViewModel的屬性。
還是舉一個例子:
public class ShellViewModel
{
public TopBarViewModel TopBar { get; private set; }
// Stuff to instantiate and assign TopBarViewModel
}
<Window x:Class="MyNamespace.ShellView"
xmlns:s="https://github.com/canton7/Stylet" .....>
<StackPanel>
<ContentControl s:View.Model="{Binding TopBar}"/>
<!-- ... -->
</StackPanel>
</Window>
View.Model附加屬性從其ViewModel的綁定中獲取ViewModel(此例中是TopBarViewModel的一個實例),然后定位到正確的View上(TopBarView)。通過這種方式實例化,將內容設置到ContentControl中。
此例中,TopBarView即可以從其TopBarViewModel中獲取當前頁面的名稱,也可通過ShellViewModel獲得頁面名稱的通知,問題得到了解決!
同樣,ContentControl在Navigation中也工作得很好:
<Window x:Class="MyNamespace.ShellView"
xmlns:s="https://github.com/canton7/Stylet" .....>
<StackPanel>
<ContentControl s:View.Model="{Binding TopBar}"/>
<ContentControl s:View.Model="{Binding CurrentPage}"/>
</StackPanel>
</Window>
ShellViewModel通過實例化一個頁面的ViewModel導航到一個新的頁面中,然后將此實例分配給屬性CurrentPage。注意ShellViewModel不再需要知道任何關於視圖(views)的信息,沒必要再去實例化一個單獨的view了,這一點非常重要,也非常有用。
對話框(Dialogs)和窗體(Windows)也可以通過WindowManager用同樣的方法處理。只需要傳遞給出的ViewModel實例,對話框或窗體的View就會顯示出來。
刪除Code-Behind!
通過這一系列操作,沒必要再寫codebehind的代碼了。通過使用Actions(處理事件),Converters,附加屬性和附件行為,刪除Code-Behind完全可以!