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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (25投票s)

2010年7月8日

Ms-PL

8分钟阅读

viewsIcon

93245

downloadIcon

3019

使用 ViewModel 风格和 Silverlight Tab Control 创建多个动态视图。

实时示例点击此处

另请参阅

动态创建视图

在使用“ViewModel 风格”编程时,您可能会发现需要动态创建视图。挑战在于动态创建它们,同时允许设计人员轻松设计 UI。

View Model 风格

ViewModel 风格允许程序员创建一个完全没有 UI(用户界面)的应用程序。程序员只需创建 ViewModel 和 Model。完全没有编程能力的设计人员可以从一张白纸开始,在 Microsoft Expression Blend 4(或更高版本)中完全创建 View(UI)。

img32.jpg

如果您是 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 WaddellShawn 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

注意:此方法首先将所有 TabItemIsSelected 设置为 false,然后将新的 TabItemIsSelected 设置为 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) 方法创建 View TaskDetails 和其 ViewModel TaskDetailsModel 的实例,并将其当前 Task 设置为选定的 Task(使用 SetCurrentTask(Task) 方法)。
  • 然后,它将此 View 放置在一个动态创建的 TabItem 上(它将 View 设置为 TabItemContent)。
  • 然后,它返回 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 作为参数传递(实际上,它们传递的是 TabItemContent,即 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 通信,希望您觉得它易于理解。此外,我们还介绍了动态创建视图,同时允许设计人员完全轻松地访问并完全更改应用程序的外观。

Silverlight RIA Tasks 2:动态 ViewModel - CodeProject - 代码之家
© . All rights reserved.