使用 Silverlight DataGrid 和 View Model / MVVM






4.82/5 (48投票s)
使用 View Model,实现 Silverlight DataGrid 的内联编辑、分页、排序和按钮事件。
另请参阅
- RIATasks: 一个简单的 Silverlight CRUD 示例
- 主视图模型与子视图模型之间的通信
- 中心 Silverlight 业务规则验证
- ** 使用行内按钮删除 Silverlight DataGrid 行 **
视图模型与 Silverlight DataGrid
Silverlight DataGrid 是一个强大的控件,支持内联编辑、分页、排序以及拖放列重新排序。使用传统的代码隐藏技术理解它可能具有挑战性,而使用视图模型/MVVM 风格编程时,有时则令人费解。
在接下来的示例中,我们不会直接在视图模型中引用 DataGrid。我们将创建的代码可以被任何集合控件(如 ListBox)使用。
注意:如果您是视图模型风格编程的新手,建议您阅读:Silverlight 视图模型风格:一个(过于)简化的解释。
您要操作的不是 DataGrid - 而是绑定到 DataGrid 的集合
理解 Silverlight DataGrid 的一个主要方面是,它始终使用实现了 ICollectionView
的“数据视图”。根据 http://msdn.microsoft.com/en-us/library/system.componentmodel.icollectionview(VS.95).aspx
“DataGrid
控件使用此接口来访问分配给其ItemsSource
属性的数据源中的指示功能。如果ItemsSource
实现IList
但未实现ICollectionView
,则DataGrid
会将ItemsSource
包装在一个内部ICollectionView
实现中。”
在最初的示例中,我们将仅将集合绑定到 DataGrid
,并允许创建此自动 CollectionView
。在排序示例中,我们将实现自己的 CollectionViewSource
,以便能够挂钩并检测事件。
我们将发现,当我们能够控制视图模型中 DataGrid 所绑定的集合时,它将为我们提供实现所需功能所需的所有控制。
(有关 DataGrid 可用方法和属性的更多信息,请参阅: http://msdn.microsoft.com/en-us/library/system.windows.controls.datagrid_methods(v=VS.95).aspx)
本教程使用Visual Studio 2010(或更高版本)和Expression Blend 4(或更高版本)。
应用程序
该应用程序允许通过单击列标题进行排序,并通过 DataGrid 底部的按钮进行分页。
您可以拖动列标题并重新排序它们。
除非您正在编辑评论,否则评论字段仅显示前 25 个字符。
双击评论字段即可编辑评论。单击其他地方(应用程序中非编辑框的任何位置)即可保存评论。
单击清除按钮会将该行的评论字段变为****。
在 DataGrid 上显示项目
让我们先在 DataGrid 上简单地显示项目。
首先,我们创建一个带有网站的 Silverlight 项目。
然后,我们创建一个名为 RIATasks 的数据库,并添加一个名为 RIAComments 的表。
USE [RIATasks]
GO
CREATE TABLE [dbo].[RIAComments](
[CommentID] [int] IDENTITY(1,1) NOT NULL,
[Comment] [nvarchar](max) NOT NULL,
[CommentUpdateDate] [datetime] NOT NULL,
CONSTRAINT [PK_RIAComments] PRIMARY KEY CLUSTERED
(
[CommentID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
注意:下载中包含一个文件 ReadMe_DatabaseSetup.txt,其中包含表的脚本,以及创建示例数据的脚本。
向 RIADataGrid.Web 网站添加一个 LINQ to SQL 类,名为 RIATasksDB.dbml。
选择服务器资源管理器。
建立到 RIATasks 数据库的连接,并将 Tasks 表拖到对象关系设计器表面。
保存并关闭文件。
现在我们将创建一个 Web Service 方法,该方法将使用以下代码返回表中的项目。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Services;
using Microsoft.VisualBasic;
namespace RIADataGrid.Web
{
[WebService(Namespace = "http://OpenLightGroup.net/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
public class WebService : System.Web.Services.WebService
{
// Web Methods
#region GetRIAComments
[WebMethod]
public List<RIAComment> GetRIAComments()
{
RIATasksDBDataContext DB = new RIATasksDBDataContext();
var colRIAComments = (from RIAComments in DB.RIAComments
select RIAComments).ToList();
return colRIAComments;
}
#endregion
}
}
在 Silverlight 应用程序项目中,我们添加一个服务引用。我们将服务引用命名为 wsRIARIAComments。
同时,向 Silverlight 应用程序项目添加对 Microsoft.VisualBasic 的程序集引用。
现在,我们将在 Silverlight 应用程序项目中创建一个模型,以调用 Web Service 中的 GetRIAComments
方法。
创建一个名为 Models 的文件夹,以及一个名为 RIACommentsModel.cs 的类,其代码如下:
using Microsoft.VisualBasic;
using System.Linq;
using System;
using System.Collections.Generic;
using System.ServiceModel;
using RIADataGrid.wsRIARIAComments;
using RIADataGrid;
namespace RIADataGrid
{
public class RIACommentsModel
{
#region GetRIAComments
public static void GetRIAComments(
EventHandler<GetRIACommentsCompletedEventArgs> eh)
{
// Set up web service call
WebServiceSoapClient WS = new WebServiceSoapClient();
// Set the EndpointAddress
WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());
WS.GetRIACommentsCompleted += eh;
WS.GetRIACommentsAsync();
}
#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
}
}
这将使我们能够从视图模型调用 GetRIAComments
Web Service 方法。
接下来,我们创建视图模型;这是我们的编程逻辑将包含的位置。
创建一个名为 ViewModels 的文件夹,以及一个名为 MainPageModel.cs 的类,其代码如下:
using System;
using System.Linq;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Input;
using System.Windows;
using RIADataGrid.wsRIARIAComments;
using Microsoft.VisualBasic;
using System.Windows.Controls;
using System.Windows.Data;
using System.Collections.Specialized;
namespace RIADataGrid
{
public class MainPageModel : INotifyPropertyChanged
{
public MainPageModel()
{
// The following line prevents Expression Blend
// from showing an error when in design mode
if (!DesignerProperties.IsInDesignTool)
{
// Get the RIAComments
GetRIAComments();
}
}
// Operations
#region GetRIAComments
private void GetRIAComments()
{
// Call the Model to get the collection of RIAComments
RIACommentsModel.GetRIAComments((Sender, EventArgs) =>
{
if (EventArgs.Error == null)
{
// Clear the current RIAComments
colRIAComments.Clear();
// loop thru each item
foreach (var RIAComment in EventArgs.Result)
{
// Add to the colRIAComments collection
colRIAComments.Add(RIAComment);
}
}
});
}
#endregion
// Collections
#region colRIAComments
private ObservableCollection<RIAComment> _colRIAComments
= new ObservableCollection<RIAComment>();
public ObservableCollection<RIAComment> colRIAComments
{
get { return _colRIAComments; }
private set
{
if (colRIAComments == value)
{
return;
}
_colRIAComments = value;
this.NotifyPropertyChanged("colRIAComments");
}
}
#endregion
// Utility
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
}
}
我们将在后面的部分扩展此类和其他类;目前,它仅用 Web Service 的结果填充一个名为 colRIAComments
的集合。
最后一部分是创建视图。
右键单击 MainPage.xaml,在 Expression Blend 中打开它。
在对象和时间线窗口中,单击LayoutRoot。
在其属性中,选择DataContext。
将其设置为 MainPageModel。
选择数据选项卡,然后选择从类创建示例数据...。
选择 MainPageModel。
单击资源按钮,然后选择DataGrid。
在主页面上绘制一个DataGrid。
将 colRIAComments 从示例数据拖动...
...并将其放入 DataGrid。
示例数据将显示在 DataGrid 上。
注意:如果您在对象和时间线窗口的 DataGrid 下方看不到 Columns,请关闭并重新打开 Expression Blend。当您在 Expression Blend 中重新打开项目时,您将看到 Columns 部分。
格式化列
在对象和时间线窗口中,选择Comment列。
将其属性设置为与上图匹配。
注意:重要的是将布局设置为 Width 1 和 Star。没有这些设置,Clear 按钮(在本教程后面使用)将无法工作。
接下来,在对象和时间线窗口中,选择CommentID列。将其属性设置为与上图匹配。
最后,在对象和时间线窗口中,选择CommentUpdateDate列。将其属性设置为与上图匹配。
将 ID 列移到 Comment 列的上方。
右键单击项目窗口中的 Web 项目,并将其设置为启动项目。
右键单击.aspx页面(在您的项目中,它可能名为“*...TestPage.aspx*”),并将其设置为启动。
按 F5 来构建并运行项目。项目将运行并在您的 Web 浏览器中打开。
Inline Editing
现在我们将修改项目以启用评论字段的内联编辑。实际上,由于我们在列定义中取消了IsReadOnly复选框,因此它已经启用了。我们现在将启用更新后的评论保存到数据库。
此外,我们将修改 GetRIAComments
Web Service,使其仅显示评论字段的前 25 个字符。这将允许应用程序运行得更快,因为它需要显示的数据更少。当用户编辑评论时,它将调用一个 Web Service 方法,该方法将返回完整的评论并显示给用户进行编辑。
我们还将实现一个方法,以确保我们只更新自上次检索以来未更改的记录。我们通过仅在上次更新时间匹配时才更新记录来实现这一点。
添加 Errors 属性
我们从教程《中心 Silverlight 业务规则验证》中学到,我们可以向 LINQ to SQL 表的部分类添加一个 Errors
属性。
我们创建一个名为 Classes 的文件夹和一个名为 DataBaseParticalClass.cs 的文件,并使用以下代码:
using System.Collections.Generic;
namespace RIADataGrid.Web
{
#region public partial class RIAComment
public partial class RIAComment
{
public List<string> Errors = new List<string>();
}
#endregion
}
修改 Web Service
现在我们将修改现有的 Web Service 方法并添加另外两个。
#region GetRIAComments
[WebMethod]
public List<RIAComment> GetRIAComments()
{
// Create a collection to hold the results
List<RIAComment> colResult = new List<RIAComment>();
RIATasksDBDataContext DB = new RIATasksDBDataContext();
var colRIAComments = from RIAComments in DB.RIAComments
select RIAComments;
// Loop thru the Tasks
foreach (var item in colRIAComments)
{
// Create a Task
RIAComment OutputRIAComment = new RIAComment();
// Get only the first 25 charaters of the comment
OutputRIAComment.CommentID = item.CommentID;
OutputRIAComment.Comment = Strings.Left(item.Comment, 25) + " ...";
OutputRIAComment.CommentUpdateDate = item.CommentUpdateDate;
// Add to the final results
colResult.Add(OutputRIAComment);
}
return colResult;
}
#endregion
#region GetRIAComment
[WebMethod]
public RIAComment GetRIAComment(int RIACommentID)
{
RIATasksDBDataContext DB = new RIATasksDBDataContext();
var result = (from RIAComments in DB.RIAComments
where RIAComments.CommentID == RIACommentID
select RIAComments).FirstOrDefault();
return result;
}
#endregion
#region UpdateRIAComment
[WebMethod]
public RIAComment UpdateRIAComment(RIAComment objRIAComment)
{
DateTime dtCurrentDate = DateTime.Now;
RIATasksDBDataContext DB = new RIATasksDBDataContext();
var result = (from RIAComments in DB.RIAComments
where RIAComments.CommentID == objRIAComment.CommentID
// This will only perform the update if the CommentUpdateDate matches
// the existing CommentUpdateDate in the database
where RIAComments.CommentUpdateDate == objRIAComment.CommentUpdateDate
select RIAComments).FirstOrDefault();
if (result != null)
{
result.Comment = Strings.Left(objRIAComment.Comment, 10000);
result.CommentUpdateDate = dtCurrentDate;
DB.SubmitChanges();
// Update the CommentUpdateDate on the object that will be returned
objRIAComment.CommentUpdateDate = dtCurrentDate;
}
else
{
// The record could not be found because the CommentUpdateDate did not match
// Or the record was deleted
objRIAComment.Errors.Add("The record was not updated");
}
// Update comments to only show 25 characters
objRIAComment.Comment = Strings.Left(objRIAComment.Comment, 25) + " ...";
return objRIAComment;
}
#endregion
以下是 Web 方法的概述:
GetRIAComments
- 修改为仅返回前 25 个字符,并在 Comment 末尾添加“...” 。GetRIAComment
- 检索单个 Comment。这用于在用户使用内联编辑时获取完整的 Comment。UpdateRIAComment
- 仅当 CommentUpdateDate 字段匹配时才更新 Comment。如果更新成功,它还会返回更新后的 Comment。如果不成功,Errors
属性将填充错误消息。
更新 Web 引用
右键单击 wsRIARIAComments Web 引用,然后选择更新服务引用。
更新模型
将以下方法添加到现有的模型中:
#region GetRIAComment
public static void GetRIAComment(int RIACommentID,
EventHandler<GetRIACommentCompletedEventArgs> eh)
{
// Set up web service call
WebServiceSoapClient WS = new WebServiceSoapClient();
// Set the EndpointAddress
WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());
WS.GetRIACommentCompleted += eh;
WS.GetRIACommentAsync(RIACommentID);
}
#endregion
#region UpdateRIAComment
public static void UpdateRIAComment(RIAComment objRIAComment,
EventHandler<UpdateRIACommentCompletedEventArgs> eh)
{
// Set up web service call
WebServiceSoapClient WS = new WebServiceSoapClient();
// Set the EndpointAddress
WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());
WS.UpdateRIACommentCompleted += eh;
WS.UpdateRIACommentAsync(objRIAComment);
}
#endregion
这些方法只是调用了添加的 Web Service 方法。
注意:由于 GetRIAComments
Web Service 方法的参数未更改,因此我们无需更改模型中的相应方法。
ViewModel
现在我们将更新视图模型。首先,我们将添加一个辅助类,它将帮助我们实现 ICommand
。ICommand
用于从视图向视图模型引发事件。
DelegateCommand 辅助类
创建一个名为 Classes 的文件夹和一个名为 DelegateCommand.cs 的文件。
用以下代码替换所有代码:
using System.Windows.Input;
using System;
// From http://johnpapa.net/silverlight/
// 5-simple-steps-to-commanding-in-silverlight/
namespace RIADataGrid
{
public class DelegateCommand : ICommand
{
Func<object, bool> canExecute;
Action<object> executeAction;
bool canExecuteCache;
public DelegateCommand(Action<object> executeAction,
Func<object, bool> canExecute)
{
this.executeAction = executeAction;
this.canExecute = canExecute;
}
#region ICommand Members
public bool CanExecute(object parameter)
{
bool temp = canExecute(parameter);
if (canExecuteCache != temp)
{
canExecuteCache = temp;
if (CanExecuteChanged != null)
{
CanExecuteChanged(this, new EventArgs());
}
}
return canExecuteCache;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
executeAction(parameter);
}
#endregion
}
}
此类使我们能够轻松调用 ICommand
。您可以在此处获取有关此类的更多信息:http://johnpapa.net/silverlight/5-simple-steps-to-commanding-in-silverlight/。
ViewModel
将以下属性添加到视图模型:
#region MessageVisibility
private Visibility _MessageVisibility
= Visibility.Collapsed;
public Visibility MessageVisibility
{
get { return _MessageVisibility; }
private set
{
if (_MessageVisibility == value)
{
return;
}
_MessageVisibility = value;
this.NotifyPropertyChanged("MessageVisibility");
}
}
#endregion
#region Errors
private ObservableCollection<string> _Errors
= new ObservableCollection<string>();
public ObservableCollection<string> Errors
{
get { return _Errors; }
private set
{
if (_Errors == value)
{
return;
}
_Errors = value;
this.NotifyPropertyChanged("Errors");
}
}
#endregion
这会添加一个用于保存任何返回错误属性,以及一个允许错误列表显示(或不显示)的属性。
将以下方法添加到类中:
#region GetRIAComment
private void GetRIAComment(RIAComment Comment)
{
// Call the Model to get the full RIAComment
RIACommentsModel.GetRIAComment(Comment.CommentID, (Sender, EventArgs) =>
{
if (EventArgs.Error == null)
{
// Find the comment in the colRIAComments collection
var CommentInCollection = (from comment in colRIAComments
where comment.CommentID == EventArgs.Result.CommentID
select comment).FirstOrDefault();
if (CommentInCollection != null)
{
CommentInCollection.Comment = EventArgs.Result.Comment;
}
}
});
}
#endregion
#region UpdateRIAComment
private void UpdateRIAComment(RIAComment objRIAComment)
{
// Call the Model to UpdateRIAComment the RIAComment
RIACommentsModel.UpdateRIAComment(objRIAComment, (Sender, EventArgs) =>
{
if (EventArgs.Error == null)
{
// Find the comment
var CommentInCollection = (from comment in colRIAComments
where comment.CommentID == EventArgs.Result.CommentID
select comment).FirstOrDefault();
if (CommentInCollection != null)
{
// Update the Comment
CommentInCollection.Comment = EventArgs.Result.Comment;
CommentInCollection.CommentUpdateDate = EventArgs.Result.CommentUpdateDate;
}
// Show any errors
Errors = EventArgs.Result.Errors;
// Set the visibility of the Message ListBox
MessageVisibility = (Errors.Count > 0) ?
Visibility.Visible : Visibility.Collapsed;
}
});
}
#endregion
这些方法提取 Comment 字段的完整内容,并通过调用模型中的相应方法来更新 Comment。
将以下代码添加到类中:
#region GetRIACommentsCommand
public ICommand GetRIACommentsCommand { get; set; }
public void GetRIAComments(object param)
{
GetRIAComments();
}
private bool CanGetRIAComments(object param)
{
return true;
}
#endregion
#region GetRIACommentCommand
public ICommand GetRIACommentCommand { get; set; }
public void GetRIAComment(object param)
{
GetRIAComment((RIAComment)param);
}
private bool CanGetRIAComment(object param)
{
return true;
}
#endregion
#region UpdateRIACommentCommand
public ICommand UpdateRIACommentCommand { get; set; }
public void UpdateRIAComment(object param)
{
// This is an Update
UpdateRIAComment((RIAComment)param);
}
private bool CanUpdateRIAComment(object param)
{
// Do not allow if there is no Current RIAComment
return (param as RIAComment != null);
}
#endregion
此代码实现了 ICommand
。我们将从视图使用 Behaviors 来引发这些 ICommand
。
将以下代码添加到视图模型的构造函数中:
GetRIACommentsCommand = new DelegateCommand(GetRIAComments, CanGetRIAComments);
GetRIACommentCommand = new DelegateCommand(GetRIAComment, CanGetRIAComment);
UpdateRIACommentCommand = new DelegateCommand(UpdateRIAComment, CanUpdateRIAComment);
此代码使用 DelegateCommand
辅助类来设置 ICommand
。
View
现在我们将完成视图。您需要构建项目,然后关闭并重新打开 MainPage.xaml 文件,以使 Expression Blend 能够识别新属性。
在数据上下文窗口中,单击Errors集合...
...并将其拖到设计表面。将自动创建一个 ListBox
并将其绑定到该集合。
创建 ListBox 后,调整 ListBox 的大小,使其位于右下角。
在数据上下文窗口中,单击 MessageVisibility
属性,然后将其拖放到Errors listbox 上。
将出现一个创建数据绑定框。为Property of [ListBox]选择Visibility,然后单击OK。
添加 Behaviors
从资源中,选择InvokeCommand Action Behavior。
将其拖放到对象和时间线窗口中的 DataGrid 上。
在 Behavior 的属性中,将Event Name选择为PreparingCellForEdit。
单击 Command 旁边的数据绑定图标。
将其绑定到 GetRIAComment 命令。
选择 CommandParameter 旁边的高级选项,然后从弹出菜单中选择数据绑定...。
将其绑定到 DataGrid 的 SelectedItem
。
使用另一个 InvokeCommand
Action Behavior 重复此过程:
- 将
RowEditEnded
选择为Event Name。 - 将
UpdateRIACommentsCommand
选择为Command。 - 将
DataGrid
和SelectedItem
选择为CommandParameter。
按 F5 构建并运行应用程序。您将看到评论只显示前 25 个字符。
当您双击一行时,您将看到完整的评论,并且可以对其进行修改。
如果您在数据库中修改了记录的更新时间,而您已开始编辑它,则它将不会保存,并且会显示错误。
DataGrid 分页
目前,所有记录都显示在 DataGrid 中。如果我们有很多记录,这将是一个问题(应用程序会运行得更慢)。现在我们将实现记录的分页。
在 Web Service 中,用以下代码替换 GetRIAComments
Web 方法的代码:
#region GetRIAComments
[WebMethod]
public List<RIAComment> GetRIAComments(int intPage)
{
// Create a collection to hold the results
List<RIAComment> colResult = new List<RIAComment>();
RIATasksDBDataContext DB = new RIATasksDBDataContext();
var colRIAComments = from RIAComments in DB.RIAComments
select RIAComments;
// Compute the CurrentPage
int CurrentPage = ((intPage * 5) - 5);
// Implement paging
colRIAComments = colRIAComments.Skip(CurrentPage).Take(5);
// Loop thru the Tasks
foreach (var item in colRIAComments)
{
// Create a Task
RIAComment OutputRIAComment = new RIAComment();
// Get only the first 25 charaters of the comment
OutputRIAComment.CommentID = item.CommentID;
OutputRIAComment.Comment = Strings.Left(item.Comment, 25) + " ...";
OutputRIAComment.CommentUpdateDate = item.CommentUpdateDate;
// Add to the final results
colResult.Add(OutputRIAComment);
}
return colResult;
}
#endregion
右键单击 wsRIARIAComments Web 引用,然后选择更新服务引用。
将模型中的 GetRIAComments
方法修改为以下内容:
#region GetRIAComments
public static void GetRIAComments(int intPage,
EventHandler<GetRIACommentsCompletedEventArgs> eh)
{
// Set up web service call
WebServiceSoapClient WS = new WebServiceSoapClient();
// Set the EndpointAddress
WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());
WS.GetRIACommentsCompleted += eh;
WS.GetRIACommentsAsync(intPage);
}
#endregion
在视图模型中,添加以下属性:
#region CurrentPage
private int _CurrentPage = 1;
public int CurrentPage
{
get { return _CurrentPage; }
private set
{
if (CurrentPage == value)
{
return;
}
_CurrentPage = value;
this.NotifyPropertyChanged("CurrentPage");
}
}
#endregion
将 GetRIAComments
方法修改为以下内容:
#region GetRIAComments
private void GetRIAComments()
{
// Call the Model to get the collection of RIAComments
RIACommentsModel.GetRIAComments(CurrentPage, (Sender, EventArgs) =>
{
if (EventArgs.Error == null)
{
// Clear the current RIAComments
colRIAComments.Clear();
// loop thru each item
foreach (var RIAComment in EventArgs.Result)
{
// Add to the colRIAComments collection
colRIAComments.Add(RIAComment);
}
}
});
}
#endregion
添加以下 ICommand
:
#region PreviousPageCommand
public ICommand PreviousPageCommand { get; set; }
public void PreviousPage(object param)
{
CurrentPage--;
GetRIAComments();
}
private bool CanPreviousPage(object param)
{
// Must not already be on the first page
return (CurrentPage > 1);
}
#endregion
#region NextPageCommand
public ICommand NextPageCommand { get; set; }
public void NextPage(object param)
{
CurrentPage++;
GetRIAComments();
}
private bool CanNextPage(object param)
{
// There must be records to move to the next page
return (colRIAComments.Count > 0);
}
#endregion
最后,将以下代码添加到构造函数中:
PreviousPageCommand = new DelegateCommand(PreviousPage, CanPreviousPage);
NextPageCommand = new DelegateCommand(NextPage, CanNextPage);
View
在页面上绘制两个按钮并标记它们。
将 InvokeCommandAction
Behavior 拖到每个按钮上。
在每个Behavior的属性中,将EventName选择为Click,然后单击Command旁边的高级选项框。
选择数据绑定...。
并绑定到相应的命令(PreviousPageCommand
或 NextPageCommand
)。
对另一个按钮重复此过程。
分页现在可以工作了。
从 DataGrid 内的按钮单击调用 ICommand
接下来,我们想在 DataGrid 的每一行上放置一个按钮,当单击该按钮时,将当前评论设置为****。
首先,将以下代码添加到视图模型:
#region ClearCommand
public ICommand ClearCommand { get; set; }
public void Clear(object param)
{
// Clear the Comment
RIAComment objRIAComment = (RIAComment)param;
objRIAComment.Comment = "****";
}
private bool CanClear(object param)
{
// Do not allow if there is no Current RIAComment
return (param as RIAComment != null);
}
#endregion
在视图模型的构造函数中添加以下行:
ClearCommand = new DelegateCommand(Clear, CanClear);
右键单击 DataGrid,然后在对象和时间线窗口中,创建一个 DataGridTextColumn
。
添加列后,将其拖动到第一列。
注意,如果您收到值不在预期的范围内。错误...
您有一个重复或编号错误的 DisplayIndex
。您可以通过在 XAML 视图中正确编号列(从第一列的 0 开始)来修复此问题。
重要的是 DataGrid 中的每一行都已绑定到数据;否则,在尝试运行项目时会收到值不能为空的错误。
在(刚刚添加的)新行的属性中,选择Binding旁边的高级选项。
将其绑定到 colRIAComments。
设置绑定后,将其余属性设置为与上图匹配。
再次右键单击该列,然后复制编辑Edit CellStyle。
在创建样式资源框中,选择新建...。
点击**确定**。
返回创建样式资源框,单击OK。
现在您正在编辑样式;右键单击样式并选择编辑当前。
单击ContentPresenter。这将移动主设计页面,以便您编辑内容模板。
在设计页面上绘制一个 Button。
在Button的属性中:
- 将Margins设置为5。
- 将Content设置为Clear。
- 单击Button,然后设置DataContext...
...到主页面使用的ViewModel(MainPageModel
)。
您这样做是为了让您将在下一步添加的Behavior能够看到主视图模型上的ICommand
。
否则,Button(及其上附加的任何 Behaviors)的作用域将仅限于 DataGrid 中行的元素。
但是,请注意,我们正在实例化 MainPageModel
的新实例。我们将引发的 ICommand
不会在连接到 UI 所在的 .xaml 页面的 MainPageModel
实例上,而是在另一个副本上。
另请注意,将为每一行创建一个新的 MainPageModel
实例,并且在此代码示例中,类构造函数中有一个调用来获取 RIAComments,该调用将每次被调用。因此,当此应用程序加载时,它将额外调用 6 次 GetRIAComments
Web Service。它只会这样做(在第一页上),但您可能根本不喜欢此行为。
解决方案是创建一个单独的视图模型,如果它被实例化多次,则不会产生任何不必要的副作用。我在此教程中涵盖了如何实现这一点:使用行内按钮删除 Silverlight DataGrid 行。
将 InvokeCommandAction
Behavior 拖到Button上。
在Behavior的属性中,将EventName设置为Click,并将Command绑定到ClearCommand。
但是,您现在的问题是,Button 使用“主页面的作用域”,因此您不再能够访问当前所在的行,因此您无法将其作为参数传递。
没问题,contentPresenter
(Button 的父级)仍然绑定到“当前行的上下文”,因此您可以将其 DataContext 作为参数传递。
在Behavior的属性中,单击CommandParameter旁边的高级选项。
将参数绑定到 ContentPresenter 的 DataContext。
按 F5 构建并运行项目。
当您单击一行的Clear按钮时,该行的Comment将变为****。
您可以轻松地将 Button 引发的 ICommand
更改为更新或删除 Comment。
排序
我们将排序留到最后,因为它实际上需要最多的代码。但是,在这种情况下,我们根本不会修改视图(除了更改 DataGrid 的绑定)。我们需要做的是创建一个自己的集合,该集合实现了 ICollectionView
。
我们需要这样做的原因是为了能够挂钩到我们集合中的事件,并检测用户何时正在对 DataGrid 进行排序。目前,DataGrid 会进行排序。但是,它只对当前页进行排序。我们不直接允许 DataGrid 自动处理排序,而是调用 Web Service 并在此处对记录进行排序,然后提取已应用排序的当前页面。
这将提供正确的排序体验。例如,如果您在第二页并且按 ID 号排序记录,您将看到整个排序集合的第二页。
修改 GetRIAComments
Web Service 方法,以允许传递排序参数并将其应用于查询。
#region GetRIAComments
[WebMethod]
public List<RIAComment> GetRIAComments(int intPage,
string SortProperty, string SortDirection)
{
// Create a collection to hold the results
List<RIAComment> colResult = new List<RIAComment>();
RIATasksDBDataContext DB = new RIATasksDBDataContext();
var colRIAComments = from RIAComments in DB.RIAComments
select RIAComments;
if (SortDirection == "Descending")
{
switch (SortProperty)
{
case "CommentID":
colRIAComments = colRIAComments.OrderByDescending(x => x.CommentID);
break;
case "Comment":
colRIAComments = colRIAComments.OrderByDescending(x => x.Comment);
break;
case "CommentUpdateDate":
colRIAComments =
colRIAComments.OrderByDescending(x => x.CommentUpdateDate);
break;
default:
colRIAComments = colRIAComments.OrderByDescending(x => x.CommentID);
break;
}
}
else
{
switch (SortProperty)
{
case "CommentID":
colRIAComments = colRIAComments.OrderBy(x => x.CommentID);
break;
case "Comment":
colRIAComments = colRIAComments.OrderBy(x => x.Comment);
break;
case "CommentUpdateDate":
colRIAComments = colRIAComments.OrderBy(x => x.CommentUpdateDate);
break;
default:
colRIAComments = colRIAComments.OrderBy(x => x.CommentID);
break;
}
}
// Compute the CurrentPage
int CurrentPage = ((intPage * 5) - 5);
// Implement paging
colRIAComments = colRIAComments.Skip(CurrentPage).Take(5);
// Loop thru the Tasks
foreach (var item in colRIAComments)
{
// Create a Task
RIAComment OutputRIAComment = new RIAComment();
// Get only the first 25 charaters of the comment
OutputRIAComment.CommentID = item.CommentID;
OutputRIAComment.Comment = Strings.Left(item.Comment, 25) + " ...";
OutputRIAComment.CommentUpdateDate = item.CommentUpdateDate;
// Add to the final results
colResult.Add(OutputRIAComment);
}
return colResult;
}
#endregion
右键单击 wsRIARIAComments Web 引用,然后选择更新服务引用。
将模型中的 GetRIAComments
方法修改为以下内容:
#region GetRIAComments
public static void GetRIAComments(int intPage, string SortProperty,
string SortDirection, EventHandler<GetRIACommentsCompletedEventArgs> eh)
{
// Set up web service call
WebServiceSoapClient WS = new WebServiceSoapClient();
// Set the EndpointAddress
WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());
WS.GetRIACommentsCompleted += eh;
WS.GetRIACommentsAsync(intPage, SortProperty, SortDirection);
}
#endregion
这允许将排序参数传递给 GetRIAComments
Web Service 方法。
将以下属性添加到视图模型:
#region ViewSource
private CollectionViewSource _ViewSource = new CollectionViewSource();
public CollectionViewSource ViewSource
{
get { return _ViewSource; }
private set
{
_ViewSource = value;
this.NotifyPropertyChanged("ViewSource");
}
}
#endregion
这提供了一个 CollectionViewSource
,我们可以将 DataGrid 绑定到它。当我们这样做时,DataGrid 将不再使用其内部的 View Source。然后,我们将能够挂钩到我们否则无法访问的排序事件。
将以下属性添加到视图模型:
#region SortProperty
private string _SortProperty;
public string SortProperty
{
get { return _SortProperty; }
private set
{
if (SortProperty == value)
{
return;
}
_SortProperty = value;
this.NotifyPropertyChanged("SortProperty");
}
}
#endregion
#region SortDirection
private string _SortDirection;
public string SortDirection
{
get { return _SortDirection; }
private set
{
if (SortDirection == value)
{
return;
}
_SortDirection = value;
this.NotifyPropertyChanged("SortDirection");
}
}
#endregion
这允许我们存储当前正在排序的字段以及排序的方向。
将以下代码添加到视图模型的构造函数中:
// Connect the ViewSource to the Comments collection
// The DataGrid will be bound to the ViewSource so that it
// can implement sorting that we can wire-up an event handler to
ViewSource.Source = colRIAComments;
// Wire-up an event handler to the SortDescriptions collection on the ViewSource
INotifyCollectionChanged sortchangeNotifier =
ViewSource.View.SortDescriptions as INotifyCollectionChanged;
// Call View_CollectionChanged when the SortDescriptions collection is changed
sortchangeNotifier.CollectionChanged +=
new NotifyCollectionChangedEventHandler(View_CollectionChanged);
此代码将 colRIAComment
s 分配给 CollectionViewSource
(ViewSource
)。
然后,它会挂钩一个处理程序到 SortDescriptions
发生变化的任何更改。当将绑定到集合的 DataGrid 更改其排序时(当用户单击 DataGrid 的标题时),CollectionViewSource
将自动更改 SortDescriptions
集合。
添加以下代码以响应事件:
#region View_CollectionChanged
void View_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// Clear the underlying collection to prevent the ViewSource
// from sorting the collection currently displayed in the ViewSource
// and then sorting it again after the web service call
colRIAComments.Clear();
// When the sort is in Add mode, it is the second (the last step)
// of defining a new Sort
if (e.Action == NotifyCollectionChangedAction.Add)
{
// re-query the datasource
GetRIAComments();
}
}
#endregion
此方法会多次调用(对于放置在 SortDescriptions
集合中的每个操作)。
NotifyCollectionChangedAction.Add
操作最后放置在集合中(在排序更改后),这时我们希望重新查询 Web Service 并提取记录,并将当前排序传递给它。
将以下方法添加到类中:
#region SetSortProperties
private void SetSortProperties()
{
// Set the Sort PropertyName and Direction
// If there is anything in the SortDescriptions collection
// Items are placed here when the Control attached to this ViewSource
// is sorted (for example, by clicking on the Header in the DataGrid)
if (ViewSource.View != null)
{
if (ViewSource.View.SortDescriptions.Count > 0)
{
SortProperty = ViewSource.View.SortDescriptions[0].PropertyName;
SortDirection = ViewSource.View.SortDescriptions[0].Direction.ToString();
}
}
}
#endregion
这将在 SortDescriptions
集合中查找正在排序的属性(字段名)和方向。它将这些值放入 SortProperty
和 SortDirection
属性(之前创建的)。这些将传递给 Web Service。
此方法将由以下更改后的方法调用:
#region GetRIAComments
private void GetRIAComments()
{
// Set Sort Properties if there are any
SetSortProperties();
// Call the Model to get the collection of RIAComments
RIACommentsModel.GetRIAComments(CurrentPage, SortProperty,
SortDirection, (Sender, EventArgs) =>
{
if (EventArgs.Error == null)
{
// Clear the current RIAComments
colRIAComments.Clear();
// loop thru each item
foreach (var RIAComment in EventArgs.Result)
{
// Add to the colRIAComments collection
colRIAComments.Add(RIAComment);
}
}
});
}
#endregion
此方法调用 Web Service 并提取记录页面。
最后一步是构建项目,然后单击 DataGrid,在属性中,在 ItemsSource 旁边,选择高级选项。
选择数据绑定...。
将其绑定到 ViewSource > View。
现在,如果您转到最后一页并进行排序,它将对整个集合进行排序,而不仅仅是出现在最后一页上的记录。
样式
您可以轻松地为整个应用程序设置主题。首先,安装Silverlight Toolkit。
定位主题。
将其拖到[UserControl]上。
应用程序将被主题化。
视图模型 - 代码更少,真的!
希望您能看到,视图模型一点也不难。一旦您看到它是如何完成的,它就真的不复杂。Expression Blend 是为了在“视图模型风格”下工作而设计的,因此当您使用这种简单的模式时,您应该会更容易使用 Expression Blend。
虽然使用代码隐藏实现 DataGrid 可能看起来更容易,但您通常会发现您需要创建大量代码来查找和修改 DataGrid 中的值和属性。
DataGrid 等控件的设计目的是绑定到集合。视图模型旨在实现绑定。是绑定为您节省了代码。一旦创建了绑定,它将自动执行功能。您不必为每个功能显式编写代码。最重要的是,您将直接绑定到您最终关心的 DataGrid 元素并从中收集参数,而不是挂钩到事件然后寻找您想要的值。
此外,您会意识到许多编程功能最好实现在底层数据源上,而不是 DataGrid 本身。
另请注意,此示例使用标准的 Web Services;您可以轻松使用 WCF 或 WCF RIA Services。视图和视图模型将保持完全相同。