Silverlight RIA 任务 2:动态视图模型






4.89/5 (25投票s)
使用 ViewModel 风格和 Silverlight Tab Control 创建多个动态视图。
实时示例:点击此处
另请参阅
动态创建视图
在使用“ViewModel 风格”编程时,您可能会发现需要动态创建视图。挑战在于动态创建它们,同时允许设计人员轻松设计 UI。
View Model 风格
ViewModel 风格允许程序员创建一个完全没有 UI(用户界面)的应用程序。程序员只需创建 ViewModel 和 Model。完全没有编程能力的设计人员可以从一张白纸开始,在 Microsoft Expression Blend 4(或更高版本)中完全创建 View(UI)。
如果您是 ViewModel 风格的新手,建议您阅读 Silverlight ViewModel 风格:一个(过于)简化的解释 以获得入门介绍。
RIA Tasks 2
本文使用了与 RIATasks:一个简单的 Silverlight CRUD 示例 中相同的代码。
虽然本文使用了相同的数据库和网站代码,但它涵盖了以下附加内容
- 动态创建视图(使用 ViewModel)
- 使用 Silverlight Tab Control
- 为动态创建的 ViewModel 创建设计时视图
- 以编程方式设置选定的 Tab
- 以编程方式创建 TabItem 并将 Tab Control 绑定到它们
- 以编程方式将动态创建的 Tab 的样式设置为静态资源
应用程序
上一个应用程序(RIATasks:一个简单的 Silverlight CRUD 示例)一次只允许您编辑一个 Task。
此应用程序允许您创建无限的 Tab,每个 Tab 都包含一个可编辑的 Task。
Web 服务
虽然我们通常使用 ViewModel 以便在设计更改时无需更改 UI,但如果基本需求发生变化,我们通常需要实际更改代码。在这种情况下,需求已发生变化。
对于网站,唯一需要更改的代码是 Web Service 代码。移除了 GetsTask
方法,并修改了 GetTasks
方法以返回 Task 描述。
[WebService(Namespace = "http://OpenLightGroup.net/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
public class WebService : System.Web.Services.WebService
{
#region GetCurrentUserID
private int GetCurrentUserID()
{
int intUserID = -1;
if (HttpContext.Current.User.Identity.IsAuthenticated)
{
// Get the current user
intUserID = Convert.ToInt32(HttpContext.Current.User.Identity.Name);
}
return intUserID;
}
#endregion
// Web Methods
#region GetTasks
[WebMethod]
public List<Task> GetTasks()
{
// Create a collection to hold the results
List<Task> colResult = new List<Task>();
RIATasksDBDataContext DB = new RIATasksDBDataContext();
var colTasks = from Tasks in DB.Tasks
where Tasks.UserID == GetCurrentUserID()
select Tasks;
return colTasks.ToList();
}
#endregion
#region DeleteTask
[WebMethod]
public string DeleteTask(int TaskID)
{
string strError = "";
RIATasksDBDataContext DB = new RIATasksDBDataContext();
try
{
var result = (from Tasks in DB.Tasks
where Tasks.TaskID == TaskID
where Tasks.UserID == GetCurrentUserID()
select Tasks).FirstOrDefault();
if (result != null)
{
DB.Tasks.DeleteOnSubmit(result);
DB.SubmitChanges();
}
}
catch (Exception ex)
{
strError = ex.Message;
}
return strError;
}
#endregion
#region UpdateTask
[WebMethod]
public string UpdateTask(Task objTask)
{
string strError = "";
RIATasksDBDataContext DB = new RIATasksDBDataContext();
try
{
var result = (from Tasks in DB.Tasks
where Tasks.TaskID == objTask.TaskID
where Tasks.UserID == GetCurrentUserID()
select Tasks).FirstOrDefault();
if (result != null)
{
result.TaskDescription = objTask.TaskDescription;
result.TaskName = objTask.TaskName;
DB.SubmitChanges();
}
}
catch (Exception ex)
{
strError = ex.Message;
}
return strError;
}
#endregion
#region InsertTask
[WebMethod]
public Task InsertTask(Task objTask)
{
RIATasksDBDataContext DB = new RIATasksDBDataContext();
try
{
Task InsertTask = new Task();
InsertTask.TaskDescription = objTask.TaskDescription;
InsertTask.TaskName = objTask.TaskName;
InsertTask.UserID = GetCurrentUserID();
DB.Tasks.InsertOnSubmit(InsertTask);
DB.SubmitChanges();
// Set the TaskID
objTask.TaskID = InsertTask.TaskID;
}
catch (Exception ex)
{
// Log the error
objTask.TaskID = -1;
objTask.TaskDescription = ex.Message;
}
return objTask;
}
#endregion
}
模型
Model 已被修改为不使用 **Rx Extensions**。虽然两种方式都能工作,但我看到了 Richard Waddell 和 Shawn Wildermuth 的代码,这些代码所需的代码量与 Rx Extensions 大致相同,但不需要添加任何额外的程序集。
public class TasksModel
{
#region GetTasks
public static void GetTasks(EventHandler<GetTasksCompletedEventArgs> eh)
{
// Set up web service call
WebServiceSoapClient WS = new WebServiceSoapClient();
// Set the EndpointAddress
WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());
WS.GetTasksCompleted += eh;
WS.GetTasksAsync();
}
#endregion
#region DeleteTask
public static void DeleteTask(int TaskID,
EventHandler<DeleteTaskCompletedEventArgs> eh)
{
// Set up web service call
WebServiceSoapClient WS = new WebServiceSoapClient();
// Set the EndpointAddress
WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());
WS.DeleteTaskCompleted += eh;
WS.DeleteTaskAsync(TaskID);
}
#endregion
#region UpdateTask
public static void UpdateTask(Task objTask,
EventHandler<UpdateTaskCompletedEventArgs> eh)
{
// Set up web service call
WebServiceSoapClient WS = new WebServiceSoapClient();
// Set the EndpointAddress
WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());
WS.UpdateTaskCompleted += eh;
WS.UpdateTaskAsync(objTask);
}
#endregion
#region InsertTask
public static void InsertTask(Task objTask,
EventHandler<InsertTaskCompletedEventArgs> eh)
{
// Set up web service call
WebServiceSoapClient WS = new WebServiceSoapClient();
// Set the EndpointAddress
WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());
WS.InsertTaskCompleted += eh;
WS.InsertTaskAsync(objTask);
}
#endregion
// Utility
#region GetBaseAddress
private static Uri GetBaseAddress()
{
// Get the web address of the .xap that launched this application
string strBaseWebAddress = App.Current.Host.Source.AbsoluteUri;
// Find the position of the ClientBin directory
int PositionOfClientBin =
App.Current.Host.Source.AbsoluteUri.ToLower().IndexOf(@"/clientbin");
// Strip off everything after the ClientBin directory
strBaseWebAddress = Strings.Left(strBaseWebAddress, PositionOfClientBin);
// Create a URI
Uri UriWebService =
new Uri(String.Format(@"{0}/WebService.asmx", strBaseWebAddress));
// Return the base address
return UriWebService;
}
#endregion
}
ViewModel
这里做了很多改变。我们现在有三个 ViewModel(也有三个 View),而不是一个。
每个 ViewModel 处理应用程序的不同部分。以下是概述
- MainPageModel.cs - 这是加载所有其他嵌入式视图的主视图的 ViewModel。Add New Task 按钮位于此 ViewModel 的视图上,但它调用了
TabControlModel
的 ViewModel 中的一个ICommand
(使用本文描述的 ViewModel 到 ViewModel 通信:Silverlight ViewModel Communication)。 - TabControlModel.cs - 此 ViewModel 动态创建
TabItem
,并将 TaskDetails 视图及其 ViewModel (TaskDetailsModel.cs) 的实例放置在TabItem
上。 - TaskDetailsModel.cs - 此 ViewModel 保存单个 Task 的详细信息。
MainPageModel
此类不包含太多代码。它主要包含一个属性(TabControlVM
),该属性将保存 TabControlModel
的实例。
这允许 MainPage
视图通过其 ViewModel (MainPageModel
) 调用 TabControlModel
中的方法。本文介绍了这种 ViewModel 到 ViewModel 的通信技术:Silverlight ViewModel Communication。
这是完整的代码
using System;
using System.ComponentModel;
namespace RIATasks
{
public class MainPageModel : INotifyPropertyChanged
{
public MainPageModel()
{
}
// Properties
#region TabControlVM
private TabControlModel _TabControlVM = new TabControlModel();
public TabControlModel TabControlVM
{
get { return _TabControlVM; }
private set
{
if (TabControlVM == value)
{
return;
}
_TabControlVM = value;
this.NotifyPropertyChanged("TabControlVM");
}
}
#endregion
// Utility
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
}
}
TabControlModel
此类执行应用程序 90% 的工作。
此 ViewModel 的主要目的是将 Task 的集合暴露给 View。在原始 RIA Tasks 应用程序中,此集合为 ObservableCollection<Task>
,但对于 RIA Tasks 2,我们决定使用 Tab Control 并公开一个 TabItem
的集合(ObservableCollection<TabItem>
)。
#region colTabItems
private ObservableCollection<TabItem> _colTabItems
= new ObservableCollection<TabItem>();
public ObservableCollection<TabItem> colTabItems
{
get { return _colTabItems; }
private set
{
if (colTabItems == value)
{
return;
}
_colTabItems = value;
this.NotifyPropertyChanged("colTabItems");
}
}
#endregion
为了填充 TabItem
的集合,我们使用 GetTasks()
方法,该方法调用 Model 并检索当前登录用户的 Tasks。
#region GetTasks
private void GetTasks()
{
// Clear the current Tasks
colTabItems.Clear();
// Call the Model to get the collection of Tasks
TasksModel.GetTasks((Param, EventArgs) =>
{
if (EventArgs.Error == null)
{
// loop thru each item
foreach (var objTask in EventArgs.Result)
{
// Create a TaskItem from the Task
TabItem objTabItem = CreateTaskItem(objTask);
// Add it to the Collection of TabItems
colTabItems.Add(objTabItem);
}
// Count the records returned
if (colTabItems.Count == 0)
{
// If there are no records, indicate that
Message = "No Records Found";
}
else
{
Message = "";
}
// If there is a CurrentTaskID set that
// as the Current task
if (CurrentTaskID != -1)
{
// Locate the Task
var objTabItem = (from TaskItem in colTabItems
let VM = (TaskItem.Content as TaskDetails).DataContext
where (VM as TaskDetailsModel).CurrentTask.TaskID == CurrentTaskID
select TaskItem).FirstOrDefault();
if (objTabItem != null)
{
// Set the CurrentTask as selected
objTabItem.IsSelected = true;
}
}
}
});
}
#endregion
ViewModel 的构造函数在 View 加载时调用 GetTasks()
方法。
public TabControlModel()
{
AddNewTaskCommand = new DelegateCommand(AddNewTask, CanAddNewTask);
DeleteTaskCommand = new DelegateCommand(DeleteTask, CanDeleteTask);
UpdateTaskCommand = new DelegateCommand(UpdateTask, CanUpdateTask);
// The following line prevents Expression Blend
// from showing an error when in design mode
if (!DesignerProperties.IsInDesignTool)
{
// Get the Tasks for the current user
GetTasks();
}
}
构造函数还设置了用于添加、更新和删除 Tasks 的 ICommand
。
这是 ICommand
的代码。
#region AddNewTaskCommand
public ICommand AddNewTaskCommand { get; set; }
public void AddNewTask(object param)
{
SetToNewTask();
}
private bool CanAddNewTask(object param)
{
// Only allow a New Task to be created
// If there are no other [New] Tasks
var colNewTasks = from Tasks in colTabItems
where (Tasks.Header as string).Contains("[New]")
select Tasks;
return (colNewTasks.Count() == 0);
}
#endregion
#region DeleteTaskCommand
public ICommand DeleteTaskCommand { get; set; }
public void DeleteTask(object param)
{
// Get The Task
Task objTask = GetTaskFromTaskDetails((param as TaskDetails));
if (objTask.TaskID != -1)
{
// Delete Task
DeleteTask(objTask);
}
else
{
RemoveTask(objTask.TaskID);
}
}
private bool CanDeleteTask(object param)
{
// Only allow this ICommand to fire
// if a TaskDetails was passed as a parameter
return ((param as TaskDetails) != null);
}
#endregion
#region UpdateTaskCommand
public ICommand UpdateTaskCommand { get; set; }
public void UpdateTask(object param)
{
// Get The Task
Task objTask = GetTaskFromTaskDetails((param as TaskDetails));
if (objTask.TaskID == -1)
{
// This is a new Task
InsertTask(objTask);
}
else
{
// This is an Update
UpdateTask(objTask);
}
}
private bool CanUpdateTask(object param)
{
// Only allow this ICommand to fire
// if a TaskDetails was passed as a parameter
return ((param as TaskDetails) != null);
}
#endregion
这些命令使用以下方法调用 Model 并执行其操作。
#region DeleteTask
private void DeleteTask(Task objTask)
{
// Call the Model to delete the Task
TasksModel.DeleteTask(objTask.TaskID, (Param, EventArgs) =>
{
if (EventArgs.Error == null)
{
// Set the Error Property
Message = EventArgs.Result;
RemoveTask(objTask.TaskID);
}
});
}
#endregion
#region UpdateTask
private void UpdateTask(Task objTask)
{
// Call the Model to UpdateTask the Task
TasksModel.UpdateTask(objTask, (Param, EventArgs) =>
{
if (EventArgs.Error == null)
{
// Set the Error Property
Message = EventArgs.Result;
}
});
}
#endregion
#region InsertTask
private void InsertTask(Task objTask)
{
// Call the Model to Insert the Task
TasksModel.InsertTask(objTask, (Param, EventArgs) =>
{
if (EventArgs.Error == null)
{
// Set the CurrentTaskID Property
// So it can be selected when Tasks re-load
CurrentTaskID = EventArgs.Result.TaskID;
// Update the Tasks list
GetTasks();
Message = "";
}
});
}
#endregion
您会注意到 AddNewTask
方法调用了 SetToNewTask()
方法。
#region SetToNewTask
private void SetToNewTask()
{
// Unset selected for all Items
foreach (var item in colTabItems)
{
item.IsSelected = false;
}
// Create a empty Task
// so form will be blank
Task objTask = new Task();
// Set TaskID = -1 so we know it's a new Task
objTask.TaskID = -1;
// Create a TaskItem from the Task
TabItem objNewTabItem = CreateTaskItem(objTask);
// Set it as selected
objNewTabItem.IsSelected = true;
// Add it to the Collection of TabItems
this.colTabItems.Add(objNewTabItem);
}
#endregion
注意:此方法首先将所有 TabItem
的 IsSelected
设置为 false
,然后将新的 TabItem
的 IsSelected
设置为 true
。这就是如何以编程方式使 Tab Control 选择一个 Tab。
SetToNewTask()
方法调用 CreateTaskItem(objTask)
方法(如下所示),该方法将 Task
转换为 TabItem
。然后,SetToNewTask()
方法将 TabItem
添加到 colTabItems
** 集合中(View 上的 Tab Control 绑定到此集合,以便显示 Tab)。
#region CreateTaskItem
private TabItem CreateTaskItem(Task Task)
{
// Create a Tasks Details
TaskDetails objTaskDetails = new TaskDetails();
// Get it's DataContext
TaskDetailsModel objTaskDetailsModel =
(TaskDetailsModel)objTaskDetails.DataContext;
// Call a method to set the Current Task at it's DataContext
objTaskDetailsModel.SetCurrentTask(Task);
// Create a TabItem
TabItem objTabItem = new TabItem();
// Give it a name so it can be programatically manipulated
objTabItem.Name = string.Format("DynamicTab_{0}", Task.TaskID.ToString());
// Set the Style to point to a Resource that the Designer
// can later change
objTabItem.Style = (Style)App.Current.Resources["TabItemStyle1"];
// Set it's Header
string strTaskID = (Task.TaskID == -1) ? "[New]" : Task.TaskID.ToString();
objTabItem.Header = String.Format("Task {0}", strTaskID);
// Set it's Content to the Tasks Details control
objTabItem.Content = objTaskDetails;
return objTabItem;
}
#endregion
CreateTaskItem(objTask)
方法创建 ViewTaskDetails
和其 ViewModelTaskDetailsModel
的实例,并将其当前Task
设置为选定的Task
(使用SetCurrentTask(Task)
方法)。- 然后,它将此 View 放置在一个动态创建的
TabItem
上(它将 View 设置为TabItem
的Content
)。 - 然后,它返回
TabItem
,以便SetToNewTask()
方法可以将其添加到colTabItems
集合中。
注意:行 objTabItem.Style = (Style)App.Current.Resources["TabItemStyle1"];
用于允许设计人员通过更改键 TabItemStyle1
的样式来修改此动态创建的 TabItem
的样式。在 RiaTasks2.zip 项目中,该样式位于“RIATasks\Assets\TabControl.xaml”文件中。
TaskDetailsModel
TaskDetailsModel
ViewModel 非常简单。它包含一个用于保存当前 Task
的属性和一个允许设置该属性的方法。
public class TaskDetailsModel : INotifyPropertyChanged
{
public TaskDetailsModel()
{
}
public void SetCurrentTask(Task param)
{
CurrentTask = param;
}
#region CurrentTask
private Task _CurrentTask = new Task();
public Task CurrentTask
{
get { return _CurrentTask; }
private set
{
if (CurrentTask == value)
{
return;
}
_CurrentTask = value;
this.NotifyPropertyChanged("CurrentTask");
}
}
#endregion
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
}
注意:此类包含一个设置 Task
属性的方法的原因是,如果动态创建 View 并尝试直接设置 Task
属性,则绑定到 Task
属性的 View 将不会更新。
View
现在,我们将创建 Views。
以下是概述。
- MainPage.xaml - 加载所有其他视图的视图。
- TabControl.xaml - 包含 Tab Control 并显示 Tab 的视图。
- TaskDetails.xaml - 在 Tab 中显示单个 Task 的视图。
MainPage View
MainPage
包含一个Add New Tasks 按钮。
我们将一个 InvokeCommandAction
行为放到按钮上。
当我们将行为的 Command
参数进行数据绑定时...
...我们看到我们可以绑定 TabControlModel
ViewModel 中的 ICommand
(存储在 TabControlVM
属性中)。
然后,我们可以转到Assets 并将 TabControl
ViewModel 拖放到页面上...
...并将其 DataContext
绑定到 TabControlVM
属性(这实现了本文所述的 ViewModel 到 ViewModel 通信:Silverlight ViewModel Communication)。
TabControl View
虽然此视图的 ViewModel 执行了应用程序的大部分工作,但视图本身非常直接。
按钮绑定到相应的 ICommand
(使用 InvokeCommandAction
行为),Tab Control 绑定到 colTabItems
集合。
需要注意的是,Update 和 Delete 按钮将当前选定的 TabItem
作为参数传递(实际上,它们传递的是 TabItem
的 Content
,即 TaskDetails
View 和 ViewModel)。
这就是方法知道要更新或删除哪个 Task 的方式。
TaskDetails View
此视图的绑定也非常简单。
然而,这展示了 ViewModel 的强大功能。
TabControlModel
创建一个Task
并将其绑定到TaskDetailsModel
的动态创建的实例。- 然后将其放入
TabItem
的集合中,Tab Control 绑定到它。 - 要更新或删除
Task
,只需将 View 作为参数传递给相应的方法。
替代样式
使用 ViewModel 的主要原因,除了它通常比使用 code-behind 风格的代码少之外,还在于它将 View 与代码解耦,并允许设计人员创建和重新创建 View 而不更改任何代码。
Alan Beasley 为 Tab 和按钮提供了一种样式。Tabs
样式对所有四个位置的 Tab 进行了样式设置(RIATasks2ABVersion.zip)。要使用它,我们只需要修改Assets 目录中的资源文件。我们还将 Tab Control 设置为将 Tab 显示在左侧。此属性是 Tab Control 的标准属性。
Haruhiro Isowa 创建了一个 Tab Control(RiaTasks2Hiro.zip),它将所有 Tab 显示在可滚动的一行中。他确实创建了代码来使他的 Tab Control 可滚动,但 RIA Tasks 2 的代码没有被修改(除了设计人员通常会修改的 *.xaml 文件)。
ViewModel - 一点也不难
希望您能看到,ViewModel 一点也不难。一旦您看到它是如何完成的,它就一点也不复杂。Expression Blend 的设计就是为了在“ViewModel 风格”下工作,因此当您使用这种简单的模式时,使用 Expression Blend 会更容易。
我们还演示了 ViewModel 通信,希望您觉得它易于理解。此外,我们还介绍了动态创建视图,同时允许设计人员完全轻松地访问并完全更改应用程序的外观。