65.9K
CodeProject 正在变化。 阅读更多。
Home

MVVM 跨平台

2010年3月22日

CPOL

2分钟阅读

viewsIcon

27203

downloadIcon

370

在开发视图模型且视图实现语言不确定时如何实现 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日:初始版本
© . All rights reserved.