MVVM 跨平台






4.13/5 (6投票s)
在开发视图模型且视图实现语言不确定时如何实现 MVVM。
引言
使用 WPF 一段时间后,我成为了 MVVM,即 Model-View-ViewModel 设计模式的忠实粉丝,这意味着数据保存在 Model 中,GUI 由 View 管理,而 ViewModel 充当 Model 和 View 之间的中介。View 使用 ViewModel 作为其 DataContext。我为自己定了一个目标,构建在 XAML .cs 文件中不使用任何代码的 View。这种技术简化了 View 需要进行的任何更改。然而,我即将面临的新任务略有不同。我被告知我即将开发的功能计划拥有基于 Web 的 UI。我的 ViewModel 不能包含任何 Window 特有的对象,例如 ICommand
。此外,新的 UI 将由我公司之外的第三方开发。该第三方可能希望使用存根来测试他们的开发。这些要求需要我重新规划。
背景
我做的第一件事是为我未来的所有 ViewModels 创建接口。View 应该知道接口,而不知道确切的实现。这使得使用存根变得更加容易。您可以在 "Interfaces" 项目中找到这些接口。第二件事是创建一个名为 InterfacesFactory
的类,它是 View 知道的唯一来自 ViewModel 的类。这个类创建所需的 ViewModels。依赖注入 也可以派上用场。
public IEditableCityViewModel GetEditableCityViewModelForAdd(Guid countryId)
{
return new EditableCityViewModel(new City(),countryId);
}
public IEditableCityViewModel GetEditableCityViewModelForEdit(Guid cityId)
{
City city = DataManager.Instance.GetCity(cityId);
return new EditableCityViewModel(city);
}
当使用需要具体类型的 DataTemplate
时,View 使用接口而不是具体类会带来问题。我通过构建我自己的 DataTemplateSelector
解决了这个问题
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
DataTemplate dTemplate = null;
if (item != null)
{
Type[] types = item.GetType().GetInterfaces();
if (types.Contains(typeof(IReadOnlyContinentViewModel)))
{
dTemplate = ContinentDataTemplate;
}
else if (types.Contains(typeof(IReadonlyCountryViewModel)))
{
dTemplate = CountryDataTemplate;
}
else if (types.Contains(typeof(IReadOnlyCityViewModel)))
{
dTemplate = CityDataTemplate;
}
}
return dTemplate;
}
<local:ReadOnlyObjectsDataTemplateSelector
x:Key="selector"
ContinentDataTemplate="{StaticResource readOnlyContinentDataItem}"
CountryDataTemplate="{StaticResource readOnlyCountryDataItem}"
CityDataTemplate="{StaticResource readOnlyCityDataTemplate}"
/>
我使用了 IList
而不是 ObservableCollection
。我使用了可以注册的事件而不是 INotifyPropertyChanged
。在这种情况下,View 将自己注册到事件中,并负责更新自身。重要的是要记住,数据只能从 Main
线程更新。
/// <summary>
/// Example for data refresh
/// </summary>
private void GetCurrentTime(object state)
{
System.Windows.Application.Current.Dispatcher.BeginInvoke
(new SimpleOperationDelegate(this.CurrentTimeUpdated));
}
private void CurrentTimeUpdated()
{
BindingExpression bindingExpression =
BindingOperations.GetBindingExpression(txtCurrentTime, TextBlock.TextProperty);
bindingExpression.UpdateTarget();
}
我没有使用 ICommand,而是在 View Model 上提供了 API 调用。由于这些 API 提交可能需要时间,因此通过异步调用来调用它们。
private void onSave(object sender, ExecutedRoutedEventArgs args)
{
this.Cursor = Cursors.Wait;
SimpleOperationDelegate delg = new SimpleOperationDelegate(this.SaveOperation);
delg.BeginInvoke(this.OperationCompleted, delg);
}
private void SaveOperation()
{
m_DataContext.Save();
}
/// <summary>
/// The operation call back. Call the dispatcher to update the view from the main thread
/// </summary>
/// <param name="result"></param>
private void OperationCompleted(IAsyncResult result)
{
System.Windows.Application.Current.Dispatcher.BeginInvoke
(new AsyncCallback(this.EndOperation), result);
}
/// <summary>
/// End the asynchronous call
/// </summary>
/// <param name="result"></param>
private void EndOperation(IAsyncResult result)
{
this.Cursor = Cursors.Arrow;
SimpleOperationDelegate delg = result.AsyncState as SimpleOperationDelegate;
delg.EndInvoke(result);
this.DialogResult = true;
this.Close();
}
Using the Code
示例解决方案是一个 TreeView
,展示了各大洲、国家和城市。可以添加、编辑或删除其中任何一个。Model 和 View 保存在 Lib 项目中。View 保存在 UILIB 项目中。有一个 Interfaces 项目,其中包含 View 使用的所有接口,还有一个 TestsLib 项目,我用于测试我的代码。数据保存在 Lib 项目中的一个 XML 文件中。编译后可以轻松运行该解决方案。
关注点
我的意图是,当时间到来时,View Model 不会被更改。我希望如此。
历史
- 2010年3月22日:初始版本