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






4.81/5 (17投票s)
使用 VC++ 和 MVVM 编写一个基本的 Windows 应用商店的“Hello World”等效程序。
引言
这是一篇关于如何使用 MVVM 模式和 Visual C++ 2012 编写 Windows 应用商店应用程序的入门文章。本文将介绍如何实现数据绑定到 ref
类,如何将视图模型与 XAML 视图关联,如何使用命令,如何在 XAML 中设置样式,如何创建和使用值转换器,以及如何使用带有 ListBox
控件的项模板。本文假定您熟悉 MVVM 和 C++/CX,并且对使用 XAML 视图有基本了解。
设置数据绑定
使类准备好进行数据绑定需要两个步骤:
- 在类上添加
[Bindable]
属性 - 在类上实现
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);
AddRestaurant
和 DeleteRestaurant
是私有类方法。
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 日 - 文章已发布