介绍 Apex
介绍 Apex - 一个轻量级的 MVVM 和实用库,支持 WPF、Silverlight 和 WP7。
引言
如果你要处理任何类型的 WPF、Silverlight 或 WP7 应用程序,那么几乎肯定会使用 MVVM。MVVM(模型-视图-视图模型)是一种设计模式,它鼓励你将表示层与数据分离。表示层在视图中完成,数据访问和操作在模型中完成,两者由视图模型绑定在一起。
本文假设你理解 MVVM 的基本原理——如果你不理解,我建议阅读 Jeremy Likness 的精彩文章《模型-视图-视图模型 (MVVM) 详解》。
如果你要使用 MVVM,市面上有一些很棒的库——然而,如果你喜欢从零开始,无论是作为学习练习,还是为了构建自己的自定义代码库,那么你将需要了解如何构建一个 MVVM 框架。在本文中,我将向你展示如何从头开始构建一个基本的 MVVM 框架。我还会演示如何创建一个通用的单一代码库,并从中创建支持 WPF、Silverlight **和** WP7 的库。
这个库叫做 **Apex**。我还有一堆已准备好发布的文章在使用 Apex——所以在我介绍它之前,我无法发布更具趣味性的内容!
现有框架
如果你正在寻找一个开箱即用的功能齐全的框架,那么我首先推荐 Cinch。Cinch 由 Sacha Barber 编写,在我看来是市面上最好的 MVVM 框架。v2 的主页 在此。另一个不错的选择是 Prism。
自己动手
准备好从零开始自己动手了吗?我们开始吧。一个基本的 MVVM 框架需要什么?
- **一个视图模型基类**:实现
INotifyPropertyChanged
并允许我们快速构建视图模型的类。 - **一个命令基类**:实现
ICommand
并允许我们快速构建命令,从而让视图能够调用视图模型的功能的类。 - **通用性**:一个允许我们在 WPF、Silverlight 和 WP7 中使用一致模式的框架。
让我们开始吧——首先,我们来看一个用作视图模型并实现 INotifyPropertyChanged
的类。
ViewModel
这是一个实现 INotifyPropertyChanged
的视图模型。
/// <summary>
/// ViewModel1 - a class that implements
/// INotifyPropertyChanged in the most basic way.
/// </summary>
public class ViewModel1 : INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
/// <summary>
/// The property changed event.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
正如我们所见,这里唯一值得关注的是我们提供了 `PropertyChanged` 事件——这是我们可以触发的事件,用于指示特定属性已更改。当我们触发此事件时,WPF 框架将知道更新绑定到指定属性的任何内容的视觉效果。
接下来,我们应该将一个调用该事件的函数放在一起——如果它已被注册。
/// <summary>
/// Raises the property changed event.
/// </summary>
/// <param name="propertyName">Name of the property.</param>
public virtual void NotifyPropertyChanged(string propertyName)
{
// Store the event handler - in case it changes between
// the line to check it and the line to fire it.
PropertyChangedEventHandler propertyChanged = PropertyChanged;
// If the event has been subscribed to, fire it.
if (propertyChanged != null)
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
现在我们有了一个可以调用来触发属性更改事件的函数。以此为起点,我们可以充实视图模型,使其真正做些事情——显示名字和姓氏。
/// <summary>
/// Gets or sets the first name.
/// </summary>
/// <value>The first name.</value>
public string FirstName
{
get { return firstName; }
set
{
if (firstName != value)
{
firstName = value;
NotifyPropertyChanged("FirstName");
}
}
}
/// <summary>
/// Gets or sets the second name.
/// </summary>
/// <value>The second name.</value>
public string SecondName
{
get { return secondName; }
set
{
if (secondName != value)
{
secondName = value;
NotifyPropertyChanged("SecondName");
}
}
}
这样,我们就拥有了一个代表名字和姓氏的视图模型。有什么问题?
- 我们必须为每个视图模型实现 `INotifyPropertyChanged`。
- 每个属性都必须进行检查,以确定它是否已更改并调用 `NotifyPropertyChanged` 函数。
我们将如何解决这个问题?
- 创建一个名为“`ViewModel`”的新类,它将成为所有未来视图模型的基础。这对于 MVVM 框架来说相当标准。
- 创建一个名为“`NotifyingProperty`”的新类,它将表示一个调用 `NotifyPropertyChanged` 的视图模型属性。这有点不同,所以我们将在这里详细介绍。
如果你一边阅读一边编码,现在是时候开始实际项目了。我将以 Apex 解决方案为基础进行描述——如果你使用此代码作为你自己的项目基础,可以在这里开始自定义内容。
创建一个名为 Apex 的新解决方案。添加一个名为 Apex 的 WPF 类库项目。在 Apex 中添加一个名为 _MVVM_ 的文件夹——这里我们将放置所有与 MVVM 相关的类。现在我们可以以上面的类为基础,将其作为我们基本视图模型类的起点。视图模型类如下所示。创建一个新文件,_ViewModel.cs_。
using System.ComponentModel;
namespace Apex.MVVM
{
/// <summary>
/// Standard viewmodel class base, simply allows
/// property change notifications to be sent.
/// </summary>
public class ViewModel : INotifyPropertyChanged
{
/// <summary>
/// The property changed event.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises the property changed event.
/// </summary>
/// <param name="propertyName">Name of the property.</param>
public virtual void NotifyPropertyChanged(string propertyName)
{
// Store the event handler - in case it changes between
// the line to check it and the line to fire it.
PropertyChangedEventHandler propertyChanged = PropertyChanged;
// If the event has been subscribed to, fire it.
if (propertyChanged != null)
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
搞定。我们有了一个可以使用的视图模型基类。那么 NotifyingProperties 呢?作为启发,让我们看看依赖属性是如何定义的。
public class SomeDependencyObject : DependencyObject
{
private static readonly DependencyProperty ageProperty =
DependencyProperty.Register("Age",
typeof(int), typeof(SomeDependencyObject));
public int Age
{
get { return (int)GetValue(ageProperty);}
set { SetValue(ageProperty, value);}
}
}
虽然有点啰嗦,但它的好处是熟悉。在任何自定义控件工作中,我们都会使用依赖属性,所以我们使用 WPF 和 Silverlight 越多,就会对上面的内容越熟悉。我们能否为视图模型的属性做类似的事情?
是的,我们可以——我们可以从 `NotifyingProperty` 类开始。它需要什么?属性的名称(字符串),属性值本身(对象),以及属性的类型。有了这些信息,我们就可以创建 `NotifyingProperty` 类。
using System;
namespace Apex.MVVM
{
/// <summary>
/// The NotifyingProperty class - represents a property of a viewmodel that
/// can be wired into the notification system.
/// </summary>
public class NotifyingProperty
{
/// <summary>
/// Initializes a new instance of the <see cref="NotifyingProperty"/> class.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="type">The type.</param>
/// <param name="value">The value.</param>
public NotifyingProperty(string name, Type type, object value)
{
Name = name;
Type = type;
Value = value;
}
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
public string Name { get; set; }
/// <summary>
/// Gets or sets the type.
/// </summary>
/// <value>The type.</value>
public Type Type { get; set; }
/// <summary>
/// Gets or sets the value.
/// </summary>
/// <value>The value.</value>
public object Value { get; set; }
}
}
这似乎是一个很好的起点——现在关于 `GetValue` 和 `SetValue` 函数,它们在 `DependencyObject` 中。如果我们向视图模型基类添加两个函数,我们可以在视图模型类中使用完全相同的语法。将以下两个函数添加到 `ViewModel` 类中。
/// <summary>
/// Gets the value of a notifying property.
/// </summary>
/// <param name="notifyingProperty">The notifying property.</param>
/// <returns>The value of the notifying property.</returns>
protected object GetValue(NotifyingProperty notifyingProperty)
{
return notifyingProperty.Value;
}
/// <summary>
/// Sets the value of the notifying property.
/// </summary>
/// <param name="notifyingProperty">The notifying property.</param>
/// <param name="value">The value to set.</param>
/// <param name="forceUpdate">If set to <c>true</c> we'll force an update
/// of the binding by calling NotifyPropertyChanged.</param>
protected void SetValue(NotifyingProperty notifyingProperty,
object value, bool forceUpdate = false)
{
// We'll only set the value and notify that it has changed if the
// value is different - or if we are forcing an update.
if (notifyingProperty.Value != value || forceUpdate)
{
// Set the value.
notifyingProperty.Value = value;
// Notify that the property has changed.
NotifyPropertyChanged(notifyingProperty.Name);
}
}
`GetValue` 很简单——它只是返回属性的值。`SetValue` 也很简单——它会检查值是否已更改,然后如果更改了,它会设置该值并触发我们之前添加的 `NotifyPropertyChanged` 函数。我们还添加了一个“`forceUpdate”参数,它允许我们在值未更改的情况下强制更新。在某些罕见的情况下我们可能需要它,但通常情况下我们不需要,所以我们将值默认设置为 `false`。
我们做到了!视图模型已完成,`NotifyingProperty` 也已完成!让我们来看看它的实际效果。
MVVMSample
在 Apex 解决方案中创建一个示例解决方案文件夹,并向其中添加一个名为 MVVMSample 的新 WPF 应用程序。我们将使用这个示例作为视图模型的演示。
添加对 Apex 库的引用,并在 MVVMSample 中创建一个名为 `MainViewModel` 的新类,如下所示。
using Apex.MVVM;
namespace MVVMSample
{
/// <summary>
/// An example view model that uses our new ViewModel base.
/// </summary>
public class MainViewModel : ViewModel
{
/// <summary>
/// The first name property.
/// </summary>
private NotifyingProperty firstNameProperty =
new NotifyingProperty("FirstName", typeof(string), string.Empty);
/// <summary>
/// The second name property.
/// </summary>
private NotifyingProperty secondNameProperty =
new NotifyingProperty("SecondName", typeof(string), string.Empty);
/// <summary>
/// Gets or sets the first name.
/// </summary>
/// <value>The first name.</value>
public string FirstName
{
get { return (string)GetValue(firstNameProperty); }
set { SetValue(firstNameProperty, value); }
}
/// <summary>
/// Gets or sets the name of the second.
/// </summary>
/// <value>The name of the second.</value>
public string SecondName
{
get { return (string)GetValue(secondNameProperty); }
set { SetValue(secondNameProperty, value); }
}
}
}
到目前为止一切顺利——我们有一个实现了基类并支持通知属性(语法与 DependencyProperties 类似)的视图模型。让我们将其连接到 MainWindow。这是主窗口代码,新增的部分以粗体显示。
<Window x:Class="MVVMSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MVVMSample"
Title="MainWindow" Height="200" Width="300">
<!-- The datacontext for the window is an instance of our viewmodel. -->
<Window.DataContext>
<local:MainViewModel x:Name="viewModel" />
</Window.DataContext>
<Grid>
<StackPanel>
<TextBlock Text="First Name" />
<TextBox Margin="4" Text="{Binding FirstName}" />
<TextBlock Text="Second Name" />
<TextBox Margin="4" Text="{Binding SecondName}" />
</StackPanel>
</Grid>
</Window>
XAML 代码紧凑整洁——事情确实朝着正确的方向发展。下一步将是处理命令。
视图模型命令
实现命令函数在此站点上的许多文章中都已完成。我想要实现的应该能够做到这一点:
- 允许将 Command 对象添加到视图模型,以便视图可以绑定到它。
- 允许 Command 对象以某种方式与视图模型的函数相关联,以便我们允许视图调用视图模型的功能。
- 允许 Command 对象向任意对象发送事件,以便我们可以让视图响应命令。这一点很重要。例如,假设我们有一个命令可以将地址簿中的当前联系人保存到数据库。我们希望确保当命令执行时,视图会将焦点移到“名字”文本框,即焦点逻辑(或任何与 UI 相关的逻辑)发生在视图中,而业务逻辑发生在视图模型中。
我们开始吧。向 Apex MVVM 文件夹添加一个名为“`ViewModelCommand”的新类,它实现 `ICommand`。
public class ViewModelCommand : ICommand
{
#region ICommand Members
public bool CanExecute(object parameter)
{
throw new NotImplementedException();
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
throw new NotImplementedException();
}
#endregion
}
这里我们看到了我们需要的最基本的东西。我们来充实一下。删除 `ICommand` 成员区域中的代码,我们将用新的代码替换它。我们添加的内容如下,逐条说明。
首先,我们需要跟踪命令是否可以执行。
/// <summary>
/// Boolean member indicating whether the command can execute.
/// </summary>
private bool canExecute = false;
查看上面的初始实现——我们需要一个 `CanExecuteChanged` 事件和一个 `CanExecute` 函数——我们现在就可以添加它们。
/// <summary>
/// Occurs when can execute is changed.
/// </summary>
public event EventHandler CanExecuteChanged;
/// <summary>
/// Defines the method that determines whether
/// the command can execute in its current state.
/// </summary>
/// <param name="parameter">Data used by the command.
/// If the command does not require data to be passed,
/// this object can be set to null.</param>
/// <returns>
/// true if this command can be executed; otherwise, false.
/// </returns>
bool ICommand.CanExecute(object parameter)
{
return canExecute;
}
让我们添加一个属性,它允许我们设置命令是否可以执行并自动触发事件。
/// <summary>
/// Gets or sets a value indicating whether this instance can execute.
/// </summary>
/// <value>
/// <c>true</c> if this instance can execute; otherwise, <c>false</c>.
/// </value>
public bool CanExecute
{
get { return canExecute; }
set
{
if (canExecute != value)
{
canExecute = value;
EventHandler canExecuteChanged = CanExecuteChanged;
if (canExecuteChanged != null)
canExecuteChanged(this, EventArgs.Empty);
}
}
}
当命令被调用时,`Execute` 将被调用。我们将添加一个名为“`DoExecute”的函数,它将实际执行命令。这意味着代码可以手动调用命令。`DoExecute` 将首先触发一个“`Executing”事件,该事件允许取消命令。这将非常有用——例如,假设我们有一个保存到数据库的命令,但我们想先弹出一个警告框。视图可以订阅“`Executing”事件,显示消息框,并在用户取消操作时设置已取消标志。这会将 UI 逻辑保留在视图中。我们还将添加一个“`Executed”事件,该事件将在命令完成时调用。这些事件中的每一个都将有一个手动编写的委托和一个手动编写的“EventArgs
”类——我们需要这个来使其兼容 Silverlight 和 WP7。
最后,Command 将允许指定一个操作——一个普通的函数或一个接受对象作为参数的函数——这就是命令执行时将要调用的。我将插入完整的类——稍后在示例中会变得更清楚。
完整的 _ViewModelCommand.cs_ 文件
using System.Windows.Input;
using System;
namespace Apex.MVVM
{
/// <summary>
/// The ViewModelCommand class - an ICommand that can fire a function.
/// </summary>
public class ViewModelCommand : ICommand
{
/// <summary>
/// Initializes a new instance of the <see cref="ViewModelCommand"/> class.
/// </summary>
/// <param name="action">The action.</param>
/// <param name="canExecute">if set to <c>true</c> [can execute].</param>
public ViewModelCommand(Action action, bool canExecute)
{
// Set the action.
this.action = action;
this.canExecute = canExecute;
}
/// <summary>
/// Initializes a new instance of the <see cref="ViewModelCommand"/> class.
/// </summary>
/// <param name="parameterizedAction">The parameterized action.</param>
/// <param name="canExecute">if set to <c>true</c> [can execute].</param>
public ViewModelCommand(Action<object> parameterizedAction, bool canExecute)
{
// Set the action.
this.parameterizedAction = parameterizedAction;
this.canExecute = canExecute;
}
/// <summary>
/// Executes the command.
/// </summary>
/// <param name="param">The param.</param>
public virtual void DoExecute(object param)
{
// Get copies of the two event handlers we'll be using.
// We get them here to protect against the event timing anti-pattern.
CancelCommandEventHandler executing = Executing;
CommandEventHandler executed = Executed;
// Do we have an 'executing' event?
if (executing != null)
{
// Call the event.
CancelCommandEventArgs args =
new CancelCommandEventArgs() { Parameter = param };
executing(this, args);
// If the event has been cancelled, bail now.
if (args.Cancel)
return;
}
// Call the action or the parameterized action, whichever has been set.
if (action != null)
action();
else if (parameterizedAction != null)
parameterizedAction(param);
// Call the executed event.
if (executed != null)
executed(this, new CommandEventArgs() { Parameter = param });
}
/// <summary>
/// The action (or parameterized action) that
/// will be called when the command is invoked.
/// </summary>
protected Action action = null;
protected Action<object> parameterizedAction = null;
/// <summary>
/// Bool indicating whether the command can execute.
/// </summary>
private bool canExecute = false;
/// <summary>
/// Gets or sets a value indicating whether this instance can execute.
/// </summary>
/// <value>
/// <c>true</c> if this instance can execute; otherwise, <c>false</c>.
/// </value>
public bool CanExecute
{
get { return canExecute; }
set
{
if (canExecute != value)
{
canExecute = value;
EventHandler canExecuteChanged = CanExecuteChanged;
if (canExecuteChanged != null)
canExecuteChanged(this, EventArgs.Empty);
}
}
}
#region ICommand Members
/// <summary>
/// Defines the method that determines whether
/// the command can execute in its current state.
/// </summary>
/// <param name="parameter">Data used by the command.
/// If the command does not require data to be passed,
/// this object can be set to null.</param>
/// <returns>
/// true if this command can be executed; otherwise, false.
/// </returns>
bool ICommand.CanExecute(object parameter)
{
return canExecute;
}
/// <summary>
/// Defines the method to be called when the command is invoked.
/// </summary>
/// <param name="parameter">Data used by the command.
/// If the command does not require data to be passed,
/// this object can be set to null.</param>
void ICommand.Execute(object parameter)
{
this.DoExecute(parameter);
}
#endregion
/// <summary>
/// Occurs when can execute is changed.
/// </summary>
public event EventHandler CanExecuteChanged;
/// <summary>
/// Occurs when the command is about to execute.
/// </summary>
public event CancelCommandEventHandler Executing;
/// <summary>
/// Occurs when the command executed.
/// </summary>
public event CommandEventHandler Executed;
}
/// <summary>
/// The CommandEventHandler delegate.
/// </summary>
public delegate void CommandEventHandler(object sender, CommandEventArgs args);
/// <summary>
/// The CancelCommandEvent delegate.
/// </summary>
public delegate void CancelCommandEventHandler(object sender,
CancelCommandEventArgs args);
/// <summary>
/// CommandEventArgs - simply holds the command parameter.
/// </summary>
public class CommandEventArgs : EventArgs
{
/// <summary>
/// Gets or sets the parameter.
/// </summary>
/// <value>The parameter.</value>
public object Parameter { get; set; }
}
/// <summary>
/// CancelCommandEventArgs - just like above but allows the event to
/// be cancelled.
/// </summary>
public class CancelCommandEventArgs : CommandEventArgs
{
/// <summary>
/// Gets or sets a value indicating whether this
/// <see cref="CancelCommandEventArgs"/> command should be cancelled.
/// </summary>
/// <value><c>true</c> if cancel;
/// otherwise, <c>false</c>.</value>
public bool Cancel { get; set; }
}
}
这里的内容相当多,但都是我们已经描述过的。让我们在 MVVMSample 中添加一个命令,该命令可以根据名字和姓氏属性构建完整姓名。对 MVVMSample 的视图模型的更改如下所示。
using Apex.MVVM;
namespace MVVMSample
{
/// <summary>
/// An example view model that uses our new ViewModel base.
/// </summary>
public class MainViewModel : ViewModel
{
public MainViewModel()
{
// Create the build name command.
buildNameCommand = new ViewModelCommand(BuildName, true);
}
private void BuildName()
{
// Set the full name.
FullName = FirstName + " " + SecondName;
}
/// <summary>
/// The first name property.
/// </summary>
private NotifyingProperty firstNameProperty =
new NotifyingProperty("FirstName", typeof(string), string.Empty);
/// <summary>
/// The second name property.
/// </summary>
private NotifyingProperty secondNameProperty =
new NotifyingProperty("SecondName", typeof(string), string.Empty);
/// <summary>
/// The full name property.
/// </summary>
private NotifyingProperty fullNameProperty =
new NotifyingProperty("FullName", typeof(string), string.Empty);
/// <summary>
/// Gets or sets the first name.
/// </summary>
/// <value>The first name.</value>
public string FirstName
{
get { return (string)GetValue(firstNameProperty); }
set { SetValue(firstNameProperty, value); }
}
/// <summary>
/// Gets or sets the second name.
/// </summary>
/// <value>The second name.</value>
public string SecondName
{
get { return (string)GetValue(secondNameProperty); }
set { SetValue(secondNameProperty, value); }
}
/// <summary>
/// Gets or sets the full name.
/// </summary>
/// <value>The full name.</value>
public string FullName
{
get { return (string)GetValue(fullNameProperty); }
set { SetValue(fullNameProperty, value); }
}
/// <summary>
/// The build name command.
/// </summary>
private ViewModelCommand buildNameCommand;
/// <summary>
/// Gets the build name command.
/// </summary>
/// <value>The build name command.</value>
public ViewModelCommand BuildNameCommand
{
get { return buildNameCommand; }
}
}
}
我们添加了一个可以绑定到的 Command,并将其与“`BuildName”函数关联起来。现在我们可以更新视图来使用它。对 _MainWindow.xaml_ 的更改如下所示,以粗体显示。
<Window x:Class="MVVMSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MVVMSample"
Title="MainWindow" Height="200" Width="300">
<!-- The datacontext for the window is an instance of our viewmodel. -->
<Window.DataContext>
<local:MainViewModel x:Name="viewModel" />
</Window.DataContext>
<Grid>
<StackPanel>
<TextBlock Text="First Name" />
<TextBox Margin="4" Text="{Binding FirstName}" />
<TextBlock Text="Second Name" />
<TextBox Margin="4" Text="{Binding SecondName}" />
<Button Margin="4" Width="80"
Content="Build Name" Command="{Binding BuildNameCommand}" />
<TextBox Margin="4" Text="{Binding FullName}" />
</StackPanel>
</Grid>
</Window>
运行代码。它奏效了,我们得到了我们想要的一切。让我们展示一下“`CanExecute”的功能。修改名字和姓氏属性如下。
/// <summary>
/// Gets or sets the first name.
/// </summary>
/// <value>The first name.</value>
public string FirstName
{
get { return (string)GetValue(firstNameProperty); }
set
{
SetValue(firstNameProperty, value);
BuildNameCommand.CanExecute = string.IsNullOrEmpty(FirstName) ==
false && string.IsNullOrEmpty(SecondName) == false ;
}
}
/// <summary>
/// Gets or sets the second name.
/// </summary>
/// <value>The second name.</value>
public string SecondName
{
get { return (string)GetValue(secondNameProperty); }
set
{
SetValue(secondNameProperty, value);
BuildNameCommand.CanExecute = string.IsNullOrEmpty(FirstName) ==
false && string.IsNullOrEmpty(SecondName) == false ;
}
}
同时调整构造函数,以便默认禁用命令。
buildNameCommand = new ViewModelCommand(BuildName, false);
最后,调整名字和姓氏框,使它们在文本更改时绑定,而不是在框失去焦点时绑定。
<TextBox Margin="4" Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" />
...
<TextBlock Text="Second Name" />
<TextBox Margin="4" Text="{Binding SecondName, UpdateSourceTrigger=PropertyChanged}" />
我们做到了。一个扎实的视图模型基类和一个扎实的命令基类,用于关联功能。
通用性
这很棒的一点是它很容易与 Silverlight 和 WP7 集成。添加一个名为 Apex.Silverlight 的 Silverlight 类库。添加一个 MVVM 文件夹。右键单击并选择“添加文件”。选择 Apex 中 MVVM 文件夹的文件,然后选择“添加为链接”。
现在创建一个名为 Apex.WP7 的 WP7 类库。像以前一样添加 MVVM 文件链接。为了使文章长度适中,我不会包含示例应用程序——你猜怎么着——它们工作方式相同,并使用相同的 Apex 代码库。
在许多情况下,并非所有内容都可以通用——某些控件只能在 Silverlight 或 WPF 上运行。但是,框架代码、视图模型、拖放等应该在每个平台上都是通用的。
后续步骤
我拥有大量基于 Apex 并扩展 Apex 的有用代码——设计时代码、拖放代码、自定义控件、验证等。我将在接下来的几周内添加每个项目——即将推出一些非常出色的内容。