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

使用 Visual C++ 2012 进行 MVVM 简介

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (17投票s)

2013 年 3 月 23 日

CPOL

5分钟阅读

viewsIcon

47245

downloadIcon

708

使用 VC++ 和 MVVM 编写一个基本的 Windows 应用商店的“Hello World”等效程序。

引言

这是一篇关于如何使用 MVVM 模式和 Visual C++ 2012 编写 Windows 应用商店应用程序的入门文章。本文将介绍如何实现数据绑定到 ref 类,如何将视图模型与 XAML 视图关联,如何使用命令,如何在 XAML 中设置样式,如何创建和使用值转换器,以及如何使用带有 ListBox 控件的项模板。本文假定您熟悉 MVVM 和 C++/CX,并且对使用 XAML 视图有基本了解。

设置数据绑定

使类准备好进行数据绑定需要两个步骤:

  1. 在类上添加 [Bindable] 属性
  2. 在类上实现 INotifyPropertyChanged
[Bindable] 
public ref class Restaurant sealed : BindableBase
{
private:
  String^ name;
  String^ city;
  String^ notes;
  int rating;

public:
  property String^ Name 
  { 
    String^ get(); 
    void set(String^ value); 
  }

  property String^ City 
  { 
    String^ get(); 
    void set(String^ value); 
  }

  property String^ Notes 
  { 
    String^ get(); 
    void set(String^ value); 
  }

  property int Rating
  { 
    int get(); 
    void set(int value); 
  }
};

添加 Bindable 属性需要额外的一个步骤才能使代码编译。这是由于 XAML 编译器的一个怪癖,我已在此处撰文介绍。

您需要在任何一个 xxx.xaml.h 文件中添加一个 include,这样代码才能正常编译。一旦编译器在 XamlTypeInfo.g.cpp 中看到类上的 Bindable 属性,它就会自动生成数据绑定所需的样板代码。这里有一小段代码片段,展示了它为我们生成的代码。

if (typeName == L"MvvmHelloWorld.ViewModels.Restaurant")
{
    ::XamlTypeInfo::InfoProvider::XamlUserType^ userType = ref new 
      ::XamlTypeInfo::InfoProvider::XamlUserType(
        this, 
        typeName, 
        GetXamlTypeByName(L"MvvmHelloWorld.DataBinding.BindableBase"));
    userType->KindOfType = ::Windows::UI::Xaml::Interop::TypeKind::Custom;
    userType->Activator =
        []() -> Platform::Object^ 
        {
            return ref new ::MvvmHelloWorld::ViewModels::Restaurant(); 
        };
    userType->AddMemberName(L"Rating");
    userType->AddMemberName(L"Notes");
    userType->AddMemberName(L"City");
    userType->AddMemberName(L"Name");
    userType->SetIsBindable();
    return userType;
}

通常的做法是创建一个所有视图模型都实现的基类,而不是为每个可绑定数据类都实现 INotifyPropertyChanged。我使用了 BindableBase,它实现了 INotifyPropertyChanged

public ref class BindableBase : DependencyObject, INotifyPropertyChanged 
{
public:
  virtual event PropertyChangedEventHandler^ PropertyChanged;

protected:
  virtual void OnPropertyChanged(String^ propertyName);
};

OnPropertyChanged 实现相当简单。

void BindableBase::OnPropertyChanged(String^ propertyName)
{
  PropertyChanged(this, ref new PropertyChangedEventArgs(propertyName));
}

现在,您只需要实现属性体,并在 setter 方法中调用 OnPropertyChanged 即可。

String^ Restaurant::Name::get()
{
  return name;
}

void Restaurant::Name::set(String^ value)
{
  if(name != value)
  {
    name = value;
    OnPropertyChanged("Name");
  }
}

int Restaurant::Rating::get()
{
  return rating;
}

void Restaurant::Rating::set(int value)
{
  if(rating != value)
  {
    if(value < 1) 
    {
      value = 1;
    }
    else if(value > 5)
    {
      value = 5;
    }

    rating = value;
    OnPropertyChanged("Rating");
  }
}

在 XAML 中绑定到 VM 数据

对于主视图,通常会有一个主视图模型类。该类也标记为 [Bindable] 并派生自 BindableBase

[Bindable] 
public ref class MainViewModel sealed : BindableBase
{

在示例项目中,我还添加了这些属性。

property String^ Title
{
  String^ get()
  {
    return "MVVM Hello World with Visual C++";
  }
}

property IObservableVector<Restaurant^>^ Restaurants
{
  IObservableVector<Restaurant^>^ get();
}

property Restaurant^ SelectedRestaurant
{
  Restaurant^ get();
  void set(Restaurant^ value);
}

请注意,Restaurants 属性的类型是 IObservableVector<Restaurant^>。这个接口会在集合发生更改时通知侦听器,本质上是 INotifyPropertyChanged 的集合等价物。示例项目中的后台存储变量类型是 Vector<Restaurant^>,这是一个实现了 IObservableVector<T> 的库集合类型。我不想用完整的 XAML 列表来充斥文章,所以这里只展示一些相关的代码片段。

<TextBlock Style="{StaticResource HeaderTextStyle}" 
           Margin="10,10,0,10" Text="{Binding Title}" />

请注意这里的绑定,它将该控件的文本设置为 Title 属性的值。如果您以前从未使用过 XAML,这开始可能看起来有点奇怪。它能够找到 Title 属性的原因是我已将 Page 的数据上下文连接到一个 MainViewModel 类的实例。

<Page
    x:Class="MvvmHelloWorld.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:MvvmHelloWorld"
    xmlns:localVM="using:MvvmHelloWorld.ViewModels"
    xmlns:localConv="using:MvvmHelloWorld.Converters"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    DataContext="{Binding Source={StaticResource MainViewModel}}">

MainViewModel 本身在 App.xaml 中定义为静态资源。

<Application.Resources>
    <ResourceDictionary>
        <localVM:MainViewModel x:Key="MainViewModel" />
    </ResourceDictionary>
</Application.Resources>

Restaurants 属性通过数据绑定到一个使用自定义项模板的 ListBox

<ListBox ItemsSource="{Binding Restaurants}" Width="500" Margin="10" Height="500"
     HorizontalAlignment="Left"
     SelectedItem="{Binding SelectedRestaurant, Mode=TwoWay}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Vertical">
                <TextBlock Text="{Binding Name}" 
                    Style="{StaticResource RestaurantTitleTextStyle}" />
                <TextBlock Text="{Binding City}" 
                    Style="{StaticResource RestaurantSubTitleTextStyle}" />
                <TextBlock Text="{Binding Rating, Converter={StaticResource RatingConverter}}" 
                    Style="{StaticResource RestaurantSubTitleTextStyle}" />
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

请注意 ListBox 上的 SelectedItem 属性是如何与视图模型上的 SelectedRestaurant 属性进行双向绑定的。这样,VM 就可以跟踪选定的餐厅,并且可能的详细信息视图可以绑定到该属性(我们在示例中就是这样做的)。注意用于评分的转换器,我将在本文稍后讨论它。

<TextBlock Text="{Binding SelectedRestaurant.Name}" 
    Style="{StaticResource RestaurantTitleTextStyle}" />
<TextBox Text="{Binding SelectedRestaurant.City, Mode=TwoWay}" 
    Style="{StaticResource RestaurantSubTitleTextBoxStyle}" />
<TextBox Text="{Binding SelectedRestaurant.Notes, Mode=TwoWay}" 
    Style="{StaticResource RestaurantSubTitleTextBoxStyle}" />
<TextBox Text="{Binding SelectedRestaurant.Rating, Mode=TwoWay}" 
    Style="{StaticResource RestaurantSubTitleTextBoxStyle}" />

这充当了详细信息视图,并绑定到 SelectedItem 属性。因此,当用户在 ListBox 中选择一家餐厅时,此视图会自动更新。当详细信息视图被用户更新时,ListBox 中的项也会自动更新(因为是 TwoWay 绑定)。TextBox 值仅在焦点离开控件时进行绑定,因此我添加了一个虚拟按钮供用户点击以触发绑定。

<Button Content="Update" Style="{StaticResource MediumButtonStyle}" 
      Width="120" Margin="5" />

请注意,您可以编写自定义行为来使文本更改时触发绑定,但这目前不是标准行为。

使用样式

只要可能,我都已将样式应用于控件。

<TextBlock Text="{Binding Name}" 
    Style="{StaticResource RestaurantTitleTextStyle}" />
<TextBlock Text="{Binding City}" 
    Style="{StaticResource RestaurantSubTitleTextStyle}" />

<Button Content="Update" Style="{StaticResource MediumButtonStyle}" 
    Width="120" Margin="5" />

这些样式来自样式字典(在 XAML 中定义)。该字典在 App.xaml 中引用。

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Common/StandardStyles.xaml"/>
        </ResourceDictionary.MergedDictionaries>

定义样式相当直接。

<Style x:Key="RestaurantTitleTextStyle" TargetType="TextBlock">
    <Setter Property="FontSize" Value="24"/>
    <Setter Property="FontWeight" Value="Bold"/>
</Style>

<Style x:Key="RestaurantTitleGreenTextStyle" TargetType="TextBlock" 
            BasedOn="{StaticResource RestaurantTitleTextStyle}">
    <Setter Property="Foreground" Value="GreenYellow"/>
</Style>

<Style x:Key="RestaurantSubTitleTextStyle" TargetType="TextBlock">
    <Setter Property="FontSize" Value="18"/>
</Style>

<Style x:Key="RestaurantSubTitleTextBoxStyle" TargetType="TextBox">
    <Setter Property="FontSize" Value="18"/>
    <Setter Property="Margin" Value="5"/>
</Style>

<Style x:Key="MediumButtonStyle" TargetType="Button">
    <Setter Property="FontSize" Value="22"/>
</Style>

这类似于使用 CSS 为 HTML 定义样式。VS 2012 的样式编辑功能不太流畅,IDE 在编辑样式字典时提供的帮助也很少。Blend 可能功能更强大一些,但可能需要一些学习曲线和适应时间。一个很酷的功能是,您可以同时在 VS 2012 和 Blend 中打开同一个项目。

使用值转换器

如果您还记得,ListBox 项模板中的评分 UI 使用了值转换器。

<TextBlock Text="{Binding Rating, Converter={StaticResource RatingConverter}}" 
    Style="{StaticResource RestaurantSubTitleTextStyle}" />

转换器实例在页面内定义为静态资源。

<Page.Resources>
    <localConv:RatingConverter x:Key="RatingConverter" />
</Page.Resources>

转换器类基本上是 IValueConverter 的实现。

public ref class RatingConverter sealed : IValueConverter
{
public:

  virtual Object^ Convert(Object^ value, 
      TypeName targetType, Object^ parameter, String^ language)
  {
    auto boxedInt = dynamic_cast<Box<int>^>(value);
    auto intValue = boxedInt != nullptr ? boxedInt->Value : 1;

    return "Rating : " + ref new String(std::wstring(intValue, '*').c_str());
  }

  virtual Object^ ConvertBack(Object^ value, 
      TypeName targetType, Object^ parameter, String^ language)
  {
    return value;
  } 
};

我的实现只处理单向转换,将整数转换为星号字符串。这是一个相当简单的实现,但转换器可以非常强大,任何严肃的项目很可能会看到您创建大量转换器。

设置命令

我想在本文中介绍的最后一件事是命令的使用。ICommand 接口是命令对象用于支持命令绑定的接口。我使用了一个非常简单的实现,任何使用过基本 MVVM 库的人都会感到熟悉。

public delegate void ExecuteDelegate(Object^ parameter);
public delegate bool CanExecuteDelegate(Object^ parameter);

[WebHostHidden]
public ref class DelegateCommand sealed : public ICommand
{
private:
  ExecuteDelegate^ executeDelegate;
  CanExecuteDelegate^ canExecuteDelegate;
  bool lastCanExecute;    

public:
  DelegateCommand(ExecuteDelegate^ execute, CanExecuteDelegate^ canExecute);

  virtual event EventHandler<Object^>^ CanExecuteChanged;
  virtual void Execute(Object^ parameter);
  virtual bool CanExecute(Object^ parameter);
};

主 VM 类定义了几个命令属性。

property ICommand^ AddRestaurantCommand;
property ICommand^ DeleteRestaurantCommand;

命令在 VM 构造函数中初始化。

MainViewModel::MainViewModel()
{
  AddRestaurantCommand = ref new DelegateCommand(
    ref new ExecuteDelegate(this, &MainViewModel::AddRestaurant),
    nullptr);

  DeleteRestaurantCommand = ref new DelegateCommand(
    ref new ExecuteDelegate(this, &MainViewModel::DeleteRestaurant),
    nullptr);

AddRestaurantDeleteRestaurant 是私有类方法。

void MainViewModel::AddRestaurant(Object^ parameter)
{
  auto restaurant = ref new Restaurant();
  restaurant->Name = NewName;
  restaurant->City = "unassigned";
  restaurant->Notes = "unassigned";
  restaurant->Rating = 1;
  restaurants->Append(restaurant);

  SelectedRestaurant = restaurant;
}

void MainViewModel::DeleteRestaurant(Object^ parameter)
{
  if(SelectedRestaurant != nullptr)
  {
    unsigned int index;
    if(restaurants->IndexOf(SelectedRestaurant, &index))
    {
      restaurants->RemoveAt(index);
      SelectedRestaurant = nullptr;
    }
  }
}

然后,命令被连接到 XAML 中的按钮。

<Button Content="Delete" Command="{Binding DeleteRestaurantCommand}" 
    Width="120" Margin="5" Style="{StaticResource MediumButtonStyle}" />

<Button Content="Add Restaurant" Command="{Binding AddRestaurantCommand}" 
    Style="{StaticResource MediumButtonStyle}" />

结论

好了,就这些了。我确实打算写一系列文章,涵盖使用 Visual C++ 编写 Windows 应用商店应用程序的更多主题,尽管我不确定是否会按特定顺序进行。一如既往,请通过本文底部的论坛发送您的反馈和批评。谢谢。

历史

  • 2013 年 3 月 23 日 - 文章已发布
© . All rights reserved.