MVVM # 第三集






4.89/5 (18投票s)
将扩展的 MVVM 模式用于实际的 LOB 应用程序:第三部分
本系列其他文章
引言
在本系列文章的第一部分中,我介绍了自己对MVVM模式的看法,并讨论了一些在我看来在某些实现甚至模型本身中存在的不足。
在第二部分中,我介绍了我在我的MVVM#实现中使用到的基类和接口。
在本部分中,我将添加应用程序特定的类,以使我们拥有一个(非常)简单的可运行应用程序。
在第四篇文章中,我将完成应用程序,展示一个(小巧但)功能齐全的应用程序,演示一些可用功能。
模型
无论我们处理的是遗留系统还是新系统,我倾向于首先考虑数据——毕竟,如果没有正确的数据,应用程序有多酷都不重要!(也称为 GIGO)。
在我们的示例应用程序中,我们只处理Customer
。所以我们需要一个Customer
类。这将是customer
的完整详细信息,在实际系统中可能包含大量数据。但是,当我们只处理一个选择列表时,我们实际上不希望有一个包含大型Customer
对象的巨大集合,仅仅是为了显示customer
的名称。为此,我创建了“ListData
”类。CustomerListData
类将只包含我希望在选择列表中显示的customer
的基本详细信息。
由于CustomerListData
是完整Customer
数据的一个子集,为了方便起见,我实际上是从CustomerListData
继承我的Customer
数据。这意味着如果我愿意,我总是可以将CustomerListData
的集合替换为Customer
的集合。
CustomerListData.cs
namespace Model
{
/// <summary>
/// Summary information for a Customer
/// As a 'cut down' version of Customer information, this class is used
/// for lists of Customers, for example, to avoid having to get a complete
/// Customer object
/// </summary>
public class CustomerListData
{
/// <summary>
/// The unique Id assigned to this Customer in the Data Store
/// </summary>
public int? Id
{
get;
set;
}
/// <summary>
/// The Business name of the Customer
/// </summary>
public string Name
{
get;
set;
}
/// <summary>
/// Which State the Customer is in
/// </summary>
public string State
{
get;
set;
}
}
}
Customer.cs
namespace Model
{
/// <summary>
/// A Customer
/// This inherits from the CustomerSummary class, which contains the basic Customer
/// information provided in lists.
/// In real implementations this class may use lazy loading to get transactions
/// </summary>
public class Customer : CustomerListData
{
/// <summary>
/// The address of the customer.
/// </summary>
public string Address
{
get;
set;
}
public string Suburb
{
get;
set;
}
public string PostCode
{
get;
set;
}
public string Phone
{
get;
set;
}
public string Email
{
get;
set;
}
}
}
为了这篇文章,我已经将我的Model
类精简到了最基本的形式。
服务
现在我们有了一些数据对象,我们需要一种方法来检索并将它们存储到我们的数据存储中(无论是数据库、文本文件、XML文件、Web服务还是其他任何东西)。因此,在Services
项目中,我们需要创建我们的Service
接口……IcustomerService.cs
using System.Collections.Generic;
using Model;
namespace Service
{
public interface ICustomerService
{
/// <summary>
/// Return the Customer for the given id
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
Customer GetCustomer(int id);
/// <summary>
/// Return a list of Customers' List Data filtered by State
/// </summary>
/// <returns></returns>
List<CustomerListData> GetListOfCustomers(string stateFilter);
/// <summary>
/// Update a customer in the data store
/// </summary>
/// <param name="?"></param>
void UpdateCustomer(Customer data);
}
}
这将为我们的应用程序提供所需的一切。所以,让我们来实现这个接口……
CustomerService.cs
using System.Collections.Generic;
using Model;
namespace Service
{
/// <summary>
/// Provide services for retrieving and storing Customer information
/// </summary>
public class CustomerService : ICustomerService
{
/// <summary>
/// A fake database implementation so we can store and retrieve customers
/// </summary>
private List<Customer> fakeDatabaseOfCustomers;
public CustomerService()
{
// Add some data to our database
fakeDatabaseOfCustomers = new List<Customer>();
fakeDatabaseOfCustomers.Add(DummyCustomerData(1));
fakeDatabaseOfCustomers.Add(DummyCustomerData(2));
fakeDatabaseOfCustomers.Add(DummyCustomerData(3));
fakeDatabaseOfCustomers.Add(DummyCustomerData(4));
fakeDatabaseOfCustomers.Add(DummyCustomerData(5));
fakeDatabaseOfCustomers.Add(DummyCustomerData(6));
fakeDatabaseOfCustomers.Add(DummyCustomerData(7));
}
/// <summary>
/// Make a fake customer
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
private Customer DummyCustomerData(int id)
{
Customer customer = new Customer()
{
Id = id,
Address = id.ToString() + " High Street",
Suburb = "Nether Wallop",
State = (id % 2) == 0 ? "Qld" : "NSW",
Email = "Customer" + id.ToString() + "@BigFoot.Com",
Phone = "07 3333 4444",
Name = "Customer Number " + id.ToString()
};
return customer;
}
#region ICustomerService
public Customer GetCustomer(int id)
{
return fakeDatabaseOfCustomers[id - 1];
}
public List<CustomerListData> GetListOfCustomers(string stateFilter)
{
List<CustomerListData> list = new List<CustomerListData>();
foreach (var item in fakeDatabaseOfCustomers)
{
if (string.IsNullOrEmpty(stateFilter) ||
item.State.ToUpper() == stateFilter.ToUpper())
{
list.Add(new CustomerListData()
{
Id = item.Id,
Name = item.Name,
State = item.State
});
}
}
return list;
}
public void UpdateCustomer(Customer data)
{
fakeDatabaseOfCustomers[(int)data.Id - 1] = data;
}
#endregion
}
}
您会看到CustomerService
类创建了一个“伪”集合(fakeDatabaseOfCustomer
)——没有保存到任何存储库——但这足以用于演示目的。它只是为了帮助我们用一些数据运行应用程序,而无需填充数据库——不要将其与后面将要讨论的设计时数据混淆。
别忘了添加对Models
项目的引用!
视图数据
因此,我们有了数据对象(在Models
中),并且我们有一些服务来存储和检索数据。现在我们需要考虑实际的呈现。请记住,我们的ViewData
需要为用户需要看到的每个属性都拥有Observable
属性。
我们将首先考虑将使用哪些数据。
- 我们需要一个类,其中包含
Customer
的所有可编辑属性(CustomerEditViewData
)。 - 我们需要一个类,其中包含用于显示
Customer
信息列表的最小数据集(CustomerListItemViewData
)。 - 我们需要一个包含
CustomerListItemViewData
集合的类,以便我们可以显示一个列表(CustomerSelectionViewData
)。
这些类与我们将要创建的ViewModels
(以及因此与Views
)的关系或多或少是1对1的。在这种情况下,ViewData
与Model
对象之间也存在(或多或少)1对1的关系——但在更大、更复杂的应用程序中,情况并非一定如此。
我说“或多或少”,因为虽然CustomerEditViewData
映射到CustomerEditViewModel
,CustomerSelectionViewData
映射到CustomerSelectionViewModel
,但CustomerSelectionViewData
实际上只是CustomerListItemViewData
的一个集合——它根本没有自己的ViewModel
。
所有这些类都位于ViewModel
项目的ViewData
子文件夹中。
它们都继承自BaseViewData
,并使用其基类的RaisePropertyChanged
方法来通知View
任何更改。
所以,让我们从CustomerListItemViewData
开始。
using System.Windows;
namespace ViewModels
{
/// <summary>
/// A minimalist view of a Customer - for displaying in lists
/// </summary>
public class CustomerListItemViewData : BaseViewData
{
#region Private Fields
private string customerName;
private int? customerId;
private string state;
#endregion
#region Observable Properties
/// <summary>
/// The Id of the Customer represented by this item.
/// </summary>
public int? CustomerId
{
get
{
return customerId;
}
set
{
if (value != customerId)
{
customerId = value;
base.RaisePropertyChanged("CustomerId");
}
}
}
public string CustomerName
{
get
{
return customerName;
}
set
{
if (value != customerName)
{
customerName = value;
base.RaisePropertyChanged("CustomerName");
}
}
}
public string State
{
get
{
return state;
}
set
{
if (value != state)
{
state = value;
base.RaisePropertyChanged("State");
}
}
}
#endregion
#region Constructor
#endregion
}
}
这一切都相当简单——所以让我们继续CustomerSelectionViewData
,正如我们所说,它只是使用System.Collections.ObjectModel
的CustomerListItemViewData
的集合。
namespace ViewModels
{
public class CustomerSelectionViewData : BaseViewData
{
private ObservableCollection<CustomerListItemViewData> customers;
public ObservableCollection<CustomerListItemViewData> Customers
{
get
{
return customers;
}
set
{
if (value != customers)
{
customers = value;
base.RaisePropertyChanged("Customers");
}
}
}
}
}
嗯,我们现在有进展了!
我们还需要CustomerEditViewData
——这是一个大头,但概念上仍然很简单。
namespace ViewModels
{
/// <summary>
/// Editable Customer Info
/// </summary>
public class CustomerEditViewData : BaseViewData
{
#region Private Fields
private string name;
private int? customerId;
private string address;
private string suburb;
private string email;
private string postCode;
private string phone;
private string state;
#endregion
#region Observable Properties
public int? CustomerId
{
get
{
return customerId;
}
set
{
if (value != customerId)
{
customerId = value;
base.RaisePropertyChanged("CustomerId");
}
}
}
public string Name
{
get
{
return name;
}
set
{
if (value != name)
{
name = value;
base.RaisePropertyChanged("Name");
}
}
}
public string Address
{
get
{
return address;
}
set
{
if (value != address)
{
address = value;
base.RaisePropertyChanged("Address");
}
}
}
public string Suburb
{
get
{
return suburb;
}
set
{
if (suburb != value)
{
suburb = value;
base.RaisePropertyChanged("Suburb");
}
}
}
public string Email
{
get
{
return email;
}
set
{
if (email != value)
{
email = value;
base.RaisePropertyChanged("Email");
}
}
}
public string PostCode
{
get
{
return postCode;
}
set
{
if (postCode != value)
{
postCode = value;
base.RaisePropertyChanged("PostCode");
}
}
}
public string Phone
{
get
{
return phone;
}
set
{
if (phone != value)
{
phone = value;
base.RaisePropertyChanged("Phone");
}
}
}
public string State
{
get
{
return state;
}
set
{
if (state != value)
{
state = value;
base.RaisePropertyChanged("State");
}
}
}
#endregion
#region Constructor
#endregion
}
}
我们就此结束ViewData
,然后转向我们的Controller
。
ICustomerController
现在我们需要看看我们的CustomerController
。它需要执行哪些功能?
- 提供一个
CustomerSelectionViewData
对象供用户显示。 - 处理
Customer
的选择。 - 处理编辑
Customer
的请求。 - 处理更改保存时更新
Customer
。
值得仔细看看第2点和第3点。在简单的情况下,您可能会认为我们不需要这两者——毕竟,当选择了一个Customer
时,我们将编辑它;但实际上这里有两个步骤——选择和编辑——即使在这种情况下,选择也**专门**是为了编辑。
当选择customer
时,我们将发送一条消息——这将是CustomerSelectionViewModel
的工作的终点。Controller
将发送一条消息,通知所有感兴趣的方Customer
已被选择进行编辑。如果没有任何东西同时注册接收消息,并且它们确认它们可以处理编辑这个特定的客户,那么控制器就需要自己采取措施来编辑客户——通过实例化一个新的CustomerEditViewModel
和CustomerEditView
。
这听起来可能过于复杂,但我考虑的是允许我们一次打开多个CustomerEditViews
——每个视图编辑一个不同的客户。所以,如果用户选择了一个客户,所有CustomerEditViewModels
都会收到一条消息,告诉它们Customer 1234
已被选择进行编辑。大多数ViewModels
会忽略这条消息——但当前正在编辑该客户的ViewModel
可以“让它自己被发现”。
所以,这是我们的ICustomerController
接口。它位于ViewModels
项目中的BaseClasses
文件夹下(是的,我知道它不是一个类,但如果你担心,可以把文件夹名改成BaseClassesIntrefacesAndOtherNonProjectSpecificClasses
之类的!)。
namespace ViewModels
{
public interface ICustomerController : IController
{
/// <summary>
/// Return a collection of Customer information to be displayed in a list
/// </summary>
/// <returns>A collection of Customers</returns>
CustomerListViewData GetCustomerSelectionViewData(string stateFilter);
/// <summary>
/// Do whatever needs to be done when a Customer is selected (i.e. edit it)
/// </summary>
/// <param name="customerId"></param>
void CustomerSelectedForEdit(CustomerListItemViewData data, BaseViewModel daddy);
/// <summary>
/// Edit this customer Id
/// </summary>
/// <param name="customerId"></param>
void EditCustomer(int customerId, BaseViewModel daddy);
/// <summary>
/// Update Customer data in the repository
/// </summary>
/// <param name="data"></param>
void UpdateCustomer(CustomerEditViewData data);
}
}
ViewModel
嗯,现在我们有了基础,让我们开始考虑我们的第一个ViewModel。终于!
CustomerSelectionViewModel
需要显示客户列表,并允许用户选择一个。起初,就这些,所以让我们编写CustomerSelectionViewModel
类。
CustomerSelectionViewModel
using System;
using System.Windows.Input;
using Messengers;
namespace ViewModels
{
/// <summary>
/// This view model expects the user to be able to select from a list of
/// Customers, sending a message when one is selected.
/// On selection, the Controller will be asked to show
/// the details of the selected Customer
/// </summary>
public class CustomerSelectionViewModel : BaseViewModel
{
#region Properties
/// <summary>
/// Just to save us casting the base class's
/// IController to ICustomerController all the time...
/// </summary>
private ICustomerController CustomerController
{
get
{
return (ICustomerController)Controller;
}
}
#region Observable Properties
private CustomerListItemViewData selectedItem;
public CustomerListItemViewData SelectedItem
{
get
{
return selectedItem;
}
set
{
if (value != selectedItem)
{
selectedItem = value;
RaisePropertyChanged("SelectedItem");
}
}
}
#endregion
#endregion
#region Commands
#region Command Relays
private RelayCommand userSelectedItemCommand;
public ICommand UserSelectedItemCommand
{
get
{
return userSelectedItemCommand ??
(userSelectedItemCommand = new RelayCommand(() =>
ObeyUserSelectedItemCommand()));
}
}
#endregion
#region Command Handlers
private void ObeyUserSelectedItemCommand()
{
CustomerController.CustomerSelectedForEdit
(this.SelectedItem, this);
}
#endregion
#endregion
#region Constructors
/// <summary>
/// Required to allow our DesignTime version to be instantiated
/// </summary>
protected CustomerSelectionViewModel()
{
}
public CustomerSelectionViewModel(ICustomerController controller,
string stateFilter = "")
: this(controller, null, stateFilter)
{
}
/// <summary>
/// Use the base class to store the controller
/// and set the Data Context of the view (view)
/// Initialise any data that needs initialising
/// </summary>
/// <param name="controller"></param>
/// <param name="view"></param>
public CustomerSelectionViewModel(ICustomerController controller,
IView view, string stateFilter = "")
: base(controller, view)
{
controller.Messenger.Register(MessageTypes.MSG_CUSTOMER_SAVED,
new Action<Message>(RefreshList));
// Leave it for half a second before filtering on State
RefreshList();
}
#endregion
#region Private Methods
private void RefreshList(Message message)
{
RefreshList();
message.HandledStatus = MessageHandledStatus.HandledContinue;
}
/// <summary>
/// Ask for an updated list of customers based on the filter
/// </summary>
private void RefreshList()
{
ViewData =
CustomerController.GetCustomerSelectionViewData("");
}
#endregion
}
}
在CustomerSelectionViewModel
中有几点需要注意……
首先,为了避免每次都必须将BaseViewModel
的Controller
属性强制转换为ICustomerController
,我添加了一个private
属性CustomerController
。就像冲水马桶一样,它只是一个方便的功能。
我们有一个Observable
属性SelectedItem
。这是目前从呈现给用户的列表中选择的CustomerListItemViewData
——所以任何绑定到此属性的都需要通过该绑定告诉我们当前选择的是什么。
我们有一个UserSelectedItemCommand
。顾名思义,这是我们的View将在用户选择了一个项目时发送的Command
。由设计者决定这是通过点击按钮,还是通过网格列表中的每一行被点击,还是通过一些在喝了几品脱健力士啤酒后想出的古怪用户界面。
有一个无参构造函数。这是必需的,因为我想能够提供设计时数据支持——而设计时支持需要一个无参构造函数。每个ViewModel
都需要一个Controller
,所以其他构造函数需要一个ICustomerController
参数。我还允许构造函数(可选地)提供状态过滤器。这在上面的列表中没有实现,但目的是允许创建CustomerSelectionViewModel
,过滤客户以仅显示来自特定州(例如操作员所在的州)的客户。
另一个构造函数允许我们在没有注入View的情况下创建ViewModel。但没有View的ViewModel有什么用呢?嗯,一点用都没有——但这是一个好问题,说明你很关注!我们无View的构造函数将允许我们为设计时创建的View实例化一个ViewModel——例如,如果设计者决定Customer
的选择和编辑应该一起出现在一个“父”视图上,她可以这样设计,而我们需要创建一个父ViewModel来实例化CustomerSelectionViewModel
并将其分配给设计时创建的View的DataContext
。
请注意,构造函数还将我们的ViewModel注册为接收MSG_CUSTOMER_SAVED
类型的消息,并在收到消息时,使用RefreshList
方法请求Controller
提供更新的Customers
列表。这样,每当customer
在某处更新时,我们的列表都会反映任何更改。
当实例化时,我们的ViewModel还会调用其Refresh()
方法来获取初始数据进行显示。我有时会纠结于“正确”的做法——ViewModel应该在实例化时获取数据,还是Controller应该将数据注入?每种思想流派都有其优点和缺点,在这个例子中,我选择了使用“拉”方法——ViewModel从Controller拉取数据——而不是“推”方法——Controller将数据推送到ViewModel。
视图
我们现在确实应该考虑创建一个View了——这样我们不仅有东西可看,而且我们的高薪设计师也有事可做!
在Views项目中创建一个新的WPFUserControl
,名为CustomerSelectionView
。您需要将*.cs*文件中的基类从UserControl
更改为BaseView
。然后,进行您的设计。这是我的XAML。(我不是设计师!)
<view:BaseView x:Class="Views.CustomerSelectionView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:view="clr-namespace:Views"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
Background="#FF190000"
Margin="0"
Padding="1"
Height="304"
Width="229"
d:DataContext="{d:DesignInstance
Type=view:DesignTimeCustomerSelectionViewModel,
IsDesignTimeCreatable=true}">
<view:BaseView.Resources>
<view:NullToFalseBooleanConverter x:Key="NullToFalseBooleanConverter" />
<view:NullToHiddenVisibilityConverter
x:Key="NullToHiddenVisibilityConverter" />
</view:BaseView.Resources>
<StackPanel Background="#FF0096C8">
<StackPanel Orientation="Horizontal"
Margin="20,20,20,2"
Height="20">
<TextBlock>State:</TextBlock>
<TextBox Width="80"
Margin="10,0,0,0"
Text="{Binding Path=StateFilter,
UpdateSourceTrigger=PropertyChanged}"></TextBox>
</StackPanel>
<DataGrid AutoGenerateColumns="False"
Height="186"
Margin="4"
ItemsSource="{Binding ViewData.Customers}"
SelectedItem="{Binding Path=SelectedItem}"
Background="#FFE0C300"
CanUserReorderColumns="False"
AlternatingRowBackground="#E6FCFCB8"
CanUserAddRows="False"
CanUserDeleteRows="False"
CanUserResizeRows="False"
SelectionMode="Single"
IsReadOnly="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Customer"
Binding="{Binding Path=CustomerName}"
Width="*" />
<DataGridTextColumn Header="State"
Binding="{Binding Path=State}" />
</DataGrid.Columns>
</DataGrid>
<TextBlock Visibility="{Binding Path=SelectedItem,
Converter={StaticResource NullToHiddenVisibilityConverter}}">
<TextBlock.Text>
<MultiBinding StringFormat="{}Selected {0} with Id {1}">
<Binding Path="SelectedItem.CustomerName" />
<Binding Path="SelectedItem.CustomerId" />
</MultiBinding>
</TextBlock.Text></TextBlock>
<Button Content="Edit Customer"
Command="{Binding Path=UserSelectedItemCommand,
Mode=OneTime}"
Width="Auto"
HorizontalAlignment="Right"
Margin="4"
Padding="8,0,8,0"
IsEnabled="{Binding Path=SelectedItem,
Converter={StaticResource NullToFalseBooleanConverter}}" />
</StackPanel>
</view:BaseView>
如果您是按顺序跟进而不是下载项目,您会发现XAML中有几个错误。
在我们的资源部分,我们引用了两个我们尚未编写的资源:NullToFalseBooleanConverter
和NullToHiddenVisibilityConverter
。第一个的原因是我的设计师想在文本块中显示当前选定客户的ID和名称——所以显然如果没有当前选定的客户,她希望TextBlock
被隐藏。第二个的原因是设计师希望在没有客户被选中的时候禁用“编辑客户”按钮。
我将我所有的转换器都放在一个源文件中,在Views项目中的一个converters文件夹里——所以我们可以现在编写这两个简单的转换器。
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Views
{
/*
* This source file contains all the converters used.
*/
/// <summary>
/// Returns false if the object is null, true otherwise.
/// handy for using when something needs to be enabled or disabled depending on
/// whether a value has been selected from a list.
/// </summary>
[ValueConversion(typeof(object), typeof(bool))]
public class NullToFalseBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
return (value != null);
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
return null;
}
}
/// <summary>
/// Returns false if the object is null, true otherwise.
/// handy for using when something needs to be enabled or disabled depending on
/// whether a value has been selected from a list.
/// </summary>
[ValueConversion(typeof(object), typeof(Visibility))]
public class NullToHiddenVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
if (value == null)
{
return Visibility.Hidden;
}
else
{
return Visibility.Visible;
}
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
return null;
}
}
}
当这两个转换器编写好后,我们只剩下一个编译错误。那一行
d:DataContext="{d:DesignInstance Type=view:DesignTimeCustomerSelectionViewModel,
IsDesignTimeCreatable=true}">
找不到DesignTimeCustomerSelectionViewModel
类。这倒也公平,因为我们还没有编写它!
这个类是仅用于设计时类,我可以填充一些看起来逼真的数据,让我的设计师看到她在处理什么。让她看到真实数据而不是一个空网格要好得多。
using System.Collections.ObjectModel;
using ViewModels;
namespace Views
{
/// <summary>
/// This class allows us to see design time customers. Blendability R Us
/// </summary>
public class DesignTimeCustomerSelectionViewModel : CustomerSelectionViewModel
{
public DesignTimeCustomerSelectionViewModel()
{
ViewData = new CustomerListViewData();
var customers = new ObservableCollection<CustomerListItemViewData>();
customers.Add(new CustomerListItemViewData()
{
CustomerId = 1,
CustomerName = "First Customer",
State = "Qld"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 2,
CustomerName = "2nd Customer",
State = "Qld"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 3,
CustomerName = "Third Customer",
State = "NSW"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 4,
CustomerName = "Fourth Customer",
State = "SA"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 1,
CustomerName = "First Customer",
State = "Qld"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 2,
CustomerName = "2nd Customer",
State = "Qld"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 3,
CustomerName = "Third Customer",
State = "NSW"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 4,
CustomerName = "Fourth Customer",
State = "SA"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 1,
CustomerName = "First Customer",
State = "Qld"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 2,
CustomerName = "2nd Customer",
State = "Qld"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 3,
CustomerName = "Third Customer",
State = "NSW"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 4,
CustomerName = "Fourth Customer",
State = "SA"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 1,
CustomerName = "First Customer",
State = "Qld"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 2,
CustomerName = "2nd Customer",
State = "Qld"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 3,
CustomerName = "Third Customer",
State = "NSW"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 4,
CustomerName = "Fourth Customer",
State = "SA"
});
((CustomerListViewData)ViewData).Customers = customers;
}
}
}
该类本身继承自“真实”的CustomerSelectionViewModel
,并且只有一个构造函数,该构造函数创建一些虚拟数据供设计师使用。
一旦编写完成,重新生成,您应该能在设计时看到设计时数据!如设计!

看起来我们离运行程序只差一点点了——还有几件事要做,所以为什么不把View
交给你的设计师美化一下,而我们去做技术性的工作呢?
控制器 (Controller)
还记得我们之前创建了ICustomerController
接口吗?好吧,现在我们要进行一些实际的实现。在任何大型系统中,Controller可能会变得相当庞大,所以我倾向于将其分成几个部分类。主类名为CustomerController
,其他类名为CustomerController_DataRetrieval
和CustomerController_ViewManagement
。这是我发现很有用的东西之一,您可能喜欢它,或者使用不同的部分类,或者将代码放在一个带有大量#regions
的源文件中——无论您喜欢什么。我喜欢通过部分类进行逻辑分离的原因是,在多开发人员环境中,它允许我分配一个开发人员编写Controller的一个区域,而不会影响可能在Controller其他区域工作的其他开发人员。
由于Controller
是系统的中心枢纽,它需要对所有其他项目以及PresentationCore
、PresentationFramework
、WindowsBase
和System.Xaml的引用——现在就添加它们,或者如果您不相信我,就等到看到错误再添加。
CustomerController.cs
using Messengers;
using Service;
using ViewModels;
using Views;
namespace Controllers
{
/// <summary>
/// The controller 'is' the application.
/// Everything is controlled by this :
/// it instantiates Views and ViewModels
/// it retrieves and stores customers via services
///
/// But it does all this only in response to requests
/// made by the ViewModels.
///
/// e.g. a ViewModel may request a list of customers
/// e.g. a ViewModel may want to save changes to a customer
///
/// set up as a partial class for convenience
/// </summary>
public partial class CustomerController : BaseController, ICustomerController
{
private static ICustomerService CustomerService;
#region Constructors
/// <summary>
/// Private constructor - we must pass a service to the constructor
/// </summary>
private CustomerController()
{
}
/// <summary>
/// The controller needs a reference to the service layer to enable it
/// to make service calls
/// </summary>
/// <param name="customerService"></param>
public CustomerController(ICustomerService customerService)
{
CustomerService = customerService;
}
#endregion
#region Public Methods
/// <summary>
/// Main entry point of the Controller.
/// Called once (from App.xaml.cs) this will initialise the application
/// </summary>
public void Start()
{
ShowViewCustomerSelection();
}
/// <summary>
/// Edit the customer with the Id passed
/// </summary>
/// <param name="customerId">Id of the customer to be edited</param>
/// <param name="daddy">The 'parent' ViewModel who will own the
/// ViewModel that controls the Customer Edit</param>
public void EditCustomer(int customerId, BaseViewModel daddy = null)
{
//BaseView view = GetCustomerEditView(customerId, daddy);
//view.ShowInWindow(false, "Edit Customer");
}
/// <summary>
/// A Customer has been selected to be edited
/// </summary>
/// <param name="data">The CustomerListItemViewData of the selected customer
/// </param>
/// <param name="daddy">The parent ViewModel</param>
public void CustomerSelectedForEdit(CustomerListItemViewData data,
BaseViewModel daddy = null)
{
// Check in case we get a null sent to us
if (data != null && data.CustomerId != null)
{
NotificationResult result = Messenger.NotifyColleagues
(MessageTypes.MSG_CUSTOMER_SELECTED_FOR_EDIT, data);
if (result == NotificationResult.MessageNotRegistered ||
result == NotificationResult.MessageRegisteredNotHandled)
{
// Nothing was out there that handled our message,
// so we'll do it ourselves!
EditCustomer((int)data.CustomerId, daddy);
}
}
}
#endregion
}
}
主CustomerController
源文件在完成其他部分类之前会显示几个构建错误。还请注意,在EditCustomer
方法中,我注释掉了代码——因为我们还没有创建执行此功能的ViewModel或View。
CustomerController_Dataretrieval.cs
using System.Collections.ObjectModel;
using Messengers;
using Model;
using Service;
using ViewModels;
namespace Controllers
{
public partial class CustomerController
{
/// <summary>
/// Get a collection of Customers and return an Observable
/// collection of CustomerListItemViewData
/// for display in a list.
/// You could bypass this conversion if you wanted to present a
/// list of Customers by binding directly to
/// the Customer object.
/// </summary>
/// <returns></returns>
public CustomerListViewData GetCustomerSelectionViewData(string stateFilter)
{
CustomerListViewData vd = new CustomerListViewData();
vd.Customers = new ObservableCollection<CustomerListItemViewData>();
foreach (var customer in CustomerService.GetListOfCustomers(stateFilter))
{
vd.Customers.Add(new CustomerListItemViewData()
{
CustomerId = (int)customer.Id,
CustomerName = customer.Name,
State = customer.State
});
}
return vd;
}
/// <summary>
/// Get the Edit View Data for the Customer Id specified
/// </summary>
/// <param name="customerId"></param>
/// <returns></returns>
public CustomerEditViewData GetCustomerEditViewData(int customerId)
{
var customer = CustomerService.GetCustomer(customerId);
return new CustomerEditViewData()
{
CustomerId = customer.Id,
Name = customer.Name,
Address = customer.Address,
Suburb = customer.Suburb,
PostCode = customer.PostCode,
State = customer.State,
Phone = customer.Phone,
Email = customer.Email
};
}
public void UpdateCustomer(CustomerEditViewData data)
{
Customer item = new Customer()
{
Id = data.CustomerId,
Address = data.Address,
Name = data.Name,
Suburb = data.Suburb,
PostCode = data.PostCode,
Email = data.Email,
Phone = data.Phone,
State = data.State
};
CustomerService.UpdateCustomer(item);
Messenger.NotifyColleagues(MessageTypes.MSG_CUSTOMER_SAVED, data);
}
}
}
CustomerController_ViewManagement.cs
using ViewModels;
using Views;
namespace Controllers
{
public partial class CustomerController : ICustomerController
{
/// <summary>
/// The ShowView methods are private.
/// A ViewModel may request some action to take place,
/// but the Controller will decide whether this action will result
/// in some view being shown.
/// e.g. clicking a 'Search' button on a form may result
/// in a Command being sent from the
/// View (via binding) to the ViewModel; the Command handler
/// then asks the Controller to
/// Search for whatever.
/// The controller may (for example) use a service to
/// return a collection of objects. if there
/// is only a single object, then it may return a single object
/// rather than popping up a search
/// view only to have the User be presented with a single action
/// from which to select.
/// </summary>
#region Show Views
private void ShowViewCustomerSelection()
{
CustomerSelectionView v = GetCustomerSelectionView();
v.ShowInWindow(false);
}
#endregion
#region Get Views
private CustomerSelectionView GetCustomerSelectionView(BaseViewModel daddy = null)
{
CustomerSelectionView v = new CustomerSelectionView();
CustomerSelectionViewModel vm = new CustomerSelectionViewModel(this, v);
if (daddy != null)
{
daddy.ChildViewModels.Add(vm);
}
return v;
}
private BaseView GetCustomerEditView(int customerId, BaseViewModel daddy)
{
//CustomerEditView v = new CustomerEditView();
//CustomerEditViewModel vm = new CustomerEditViewModel(this, v);
//vm.ViewData = GetCustomerEditViewData(customerId);
//if (daddy != null)
//{
// daddy.ChildViewModels.Add(vm);
//}
//return v;
return new BaseView();
}
#endregion
}
}
同样,我调整了GetCustomerEditView
方法,因为我们还没有编写View或ViewModel。
生成一下,应该就没有问题了。但尝试运行它,您会看到一个未处理的IO异常“无法定位资源‘mainwindow.xaml’”。
别担心——这是预料之中的。还记得我们创建了一个WPF应用程序,它期望我们使用一个主窗口WPF窗口——我们已经删除了它吗?但我们没有告诉应用程序我们不需要它!现在就做。我们需要从CustomerMaintenance项目向Controllers项目添加引用——这样应用程序就知道在哪里可以找到它的控制器,以及向Services项目添加引用,因为Controllers在其构造函数中需要注入一个Service。我们还需要对ViewModels
的引用,因为CustomerController
接口就位于那里。
您还需要确保App.Xaml文件的生成操作属性设置为“ApplicationDefinition
”。
在Customermaintenance项目中打开您的App.xaml文件,并进行如下更改……
<Application x:Class="MyMVVMApplication.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Startup="Application_Startup">
<Application.Resources>
</Application.Resources>
</Application>
Startup=
属性需要指向我们的事件处理程序,该处理程序将启动整个过程。
最后,打开App.xaml.cs文件并进行如下更改……
using System.Windows;
using Controllers;
using Service;
using System;
namespace CustomerMaintenance
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
private void Application_Startup(object sender, StartupEventArgs e)
{
CustomerController controller = new CustomerController(new CustomerService());
controller.Start();
}
}
}
好了——还在等什么?按F5!
程序运行,一个窗体出现,显示Customer
选择,并列出customers
。
精明的WPF程序员在运行应用程序时总是会检查输出窗口。以防您是其中之一,我将指出,事实上,有一个错误。
System.Windows.Data Error: 40 : BindingExpression path error: 'StateFilter'
property not found on 'object' ''CustomerSelectionViewModel' (HashCode=13304725)'.
BindingExpression:Path=StateFilter; DataItem='CustomerSelectionViewModel'
(HashCode=13304725); target element is 'TextBox' (Name='');
target property is 'Text' (type 'String')
这仅仅是因为我在View
上留下了StateFilter TextBox
,但省略了ViewModel
中任何能够实际处理它的属性。
但让我们不要沉湎于消极,穿上您的派对礼服,庆祝一下——我们有一个可运行的MVVM#应用程序!
下次,我们将添加过滤功能,并创建CustomerEditViewModel
及其关联的View,这样我们就拥有了一个小巧但功能齐全的应用程序。