ASPxGridView 主从数据表示与上下文菜单





5.00/5 (9投票s)
DevExpress ASPxGridView控件的实际详细使用示例。
介绍
由于我之前工作过的许多公司都在使用DevExpress控件,所以我决定写几篇文章,介绍一些实际场景以及如何使用DevExpress ASP.NET控件套件来解决它们。在这篇文章以及后续的文章中,我将向您展示一些技术,以实现比您能在DevExpress网站上找到的演示示例更进一步的行为。我将使用ASP.NET控件套件,特别是Web Forms。文章结尾将展示预期的结果。

目录
什么是DevExpress控件?
一组控件,它们通过添加标准ASP.NET控件中不存在的各种控件来丰富您的工具箱,并提供一些现有的控件的良好替代品。所有DevExpress控件都拥有丰富的客户端和服务器端属性、方法和事件,让您能够实现否则需要大量额外编码才能实现的结果。要求
我的所有示例都使用.NET 4.0和Visual Studio 2010,以及v2012 vol 1.5版本的DevExpress控件编写。您可以在此处找到所需控件的试用版 DevExpress Demo。任何版本的Visual Studio都可以,从Express到Ultimate。如果您需要,也可以轻松地将此项目迁移到.NET 3.5。如果我使用的DevExpress控件版本不再可用,通过DXperience Project Converter升级项目应该很容易。有关项目转换器的更多信息,请查看DevExpress网站。项目
我将从默认的Visual Studio ASP.NET Web应用程序开始。考虑到此示例仅是关于UI和控件本身的练习,我将不强调数据源,而只创建一个非常简单的数据模型。我们将有两个数据实体:用户(User)和项目(Project)。作为基数,我们有一个多对多关系,因此一个用户可以关联到多个项目,而一个项目可以有多个用户。我还将创建一个名为DataService的类,该类将创建一些值来代表我们的数据。让我们开始吧!创建数据源
首先,我们将创建一个Project类。在默认构造函数中,我们将项目状态属性设置为“new”。除此以外,我们将拥有三个属性:ID、项目名称和项目状态。public class Project
{
public Project()
{
Status = ProjectStatus.New;
}
public int ID { get; set; }
public string Name { get; set; }
public ProjectStatus Status { get; set; }
}
如您所见,项目状态是一个枚举,所以让我们与所有其他可能的项目状态一起定义它。public enum ProjectStatus
{
New,
InProgress,
Failed,
Done
}
现在,我们将定义User类。它有几个属性和一个公共方法。该方法返回当前客户实例关联的项目数量。public class User
{
public User()
{
Projects = new List<Project>();
}
private string m_fullName;
public int ID { get; set; }
public string UserName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public List<Project> Projects { get; set; }
public string FullName
{
get { return string.Format("{0}, {1}", this.LastName, this.FirstName); }
}
public bool HasProjects()
{
return Projects.Count > 0;
}
}
这是一个非常简单的模型,在实际情况中,您的模型可能会拥有更丰富的属性和方法。接下来是一个类,它将创建模型类的多个实例并通过一个方法返回它们。这样,我们就可以轻松地获得示例所需的所有数据。我使用的代码如下,如果您正在寻找特定的行为,可以添加其他数据。[DataObject(true)]
public class DataService
{
[DataObjectMethodAttribute(DataObjectMethodType.Select, true)]
public static List<User> GetUsers()
{
List<User> users = new List<User>();
users.Add(new User() { ID = 1, UserName = "JohnDoe", FirstName = "John", LastName = "Doe", Projects = GetSomeProjects() });
users.Add(new User() { ID = 2, UserName = "JimDoe", FirstName = "Jim", LastName = "Doe", });
users.Add(new User() { ID = 3, UserName = "RobertDoe", FirstName = "Robert", LastName = "Doe", });
users.Add(new User() { ID = 4, UserName = "AlisonDoe", FirstName = "Alison", LastName = "Doe", Projects = GetSomeProjects2() });
return users;
}
[DataObjectMethodAttribute(DataObjectMethodType.Select, false)]
private static List<Project> GetSomeProjects()
{
List<Project> projects = new List<Project>();
projects.Add(new Project() { ID = 1, Name = "Test1" });
projects.Add(new Project() { ID = 2, Name = "Test2", Status = ProjectStatus.Failed });
projects.Add(new Project() { ID = 3, Name = "Test3" });
return projects;
}
[DataObjectMethodAttribute(DataObjectMethodType.Select, false)]
private static List<Project> GetSomeProjects2()
{
List<Project> projects = new List<Project>();
projects.Add(new Project() { ID = 4, Name = "Test4" });
projects.Add(new Project() { ID = 5, Name = "Test5", Status = ProjectStatus.Failed });
projects.Add(new Project() { ID = 6, Name = "Test6", Status = ProjectStatus.InProgress });
return projects;
}
}
现在我们的数据源已经准备好了。接下来要关注的是网页本身。网页
通过将ASPxGridView控件拖放到页面上来添加网格。修改属性以匹配以下内容:<dx:ASPxGridView ID="gvMaster" runat="server" AutoGenerateColumns="False" KeyFieldName="ID"
Width="100%" >
<Columns>
<dx:GridViewDataTextColumn FieldName="ID" VisibleIndex="0">
</dx:GridViewDataTextColumn>
<dx:GridViewDataTextColumn FieldName="UserName" VisibleIndex="1">
</dx:GridViewDataTextColumn>
<dx:GridViewDataTextColumn FieldName="FirstName" VisibleIndex="2">
</dx:GridViewDataTextColumn>
<dx:GridViewDataTextColumn FieldName="LastName" VisibleIndex="3">
</dx:GridViewDataTextColumn>
<dx:GridViewDataTextColumn FieldName="FullName" VisibleIndex="4">
</dx:GridViewDataTextColumn>
</Columns>
</dx:ASPxGridView>
我们所做的是将网格的ID更改为gvMaster,通过设置KeyFieldName为“ID”来指示键字段名称,并指定将显示哪些列以及将列字段名映射到所需的模型属性。现在我们需要绑定网格,我们将在代码中进行。为了能够显示更改,我们将把数据保存在Session中,为了简化此操作,我们将创建一个属性来执行所有这些检查和操作。进入页面的代码文件并声明以下内容:public List<User> Users
{
get
{
if (Session["Data"> == null)
Session["Data"> = DataService.GetUsers();
return (List<User>)Session["Data"];
}
set { Session["Data"> = value; }
}
当第一次请求该属性时,Session项为null,然后我们将调用之前创建的方法并将数据保存在Session中。这不是您将在实际应用程序中使用的技术,因为您可能会在某个时候从数据库中调用数据并最终缓存它。正如我所解释的,这不是本文的目标,我只会提及它。现在是时候将网格绑定到此属性了。protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
gvMaster.DataSource = Users;
gvMaster.DataBind();
}
}
如果现在运行代码,您应该会看到以下内容(或类似屏幕,如果应用的样式不同,请不用担心,我们稍后会处理)。

定义详细网格
要为主网格添加详细网格,我们首先需要为详细行定义一个模板。在aspx文件中,进入ASPxGridView的定义,打开<Templates>
部分,然后在新创建的部分中再创建一个名为<DetailRow>
的部分。正确关闭两个部分。在Detail Row模板中,添加一个新的ASPxGridView,并像主网格一样定义列和一些属性。同时,不要忘记将ShowDetailRow
属性设置为true。最后,您的代码应该如下所示。<SettingsDetail ShowDetailRow="True" />
<Templates>
<DetailRow>
<dx:ASPxGridView ID="gvDetail" runat="server" Width="100%" KeyFieldName="ID">
<Columns>
<dx:GridViewDataTextColumn FieldName="ID" VisibleIndex="0">
</dx:GridViewDataTextColumn>
<dx:GridViewDataTextColumn FieldName="Name" VisibleIndex="1">
</dx:GridViewDataTextColumn>
<dx:GridViewDataTextColumn FieldName="Status" VisibleIndex="2">
</dx:GridViewDataTextColumn>
</Columns>
</dx:ASPxGridView>
</DetailRow>
</Templates>
现在我们需要处理详细网格的绑定。在此之前,我们将检查主网格中的每一行,看是否有详细网格的数据,如果没有,则隐藏加号。要实现这一点,我们需要在主网格上声明OnDetailRowGetButtonVisibility
事件。您的主网格代码应如下所示:<dx:ASPxGridView ID="gvMaster" runat="server" AutoGenerateColumns="False" KeyFieldName="ID"
Width="100%" OnDetailRowGetButtonVisibility="gvMaster_DetailRowGetButtonVisibility">
在服务器端事件代码中,我们需要检查每个主元素是否有详细数据。protected void gvMaster_DetailRowGetButtonVisibility(object sender, ASPxGridViewDetailRowButtonEventArgs e)
{
User currentUser = Users.Find(u => u.ID == (int)gvMaster.GetRowValues(e.VisibleIndex, "ID"));
if (!currentUser.HasProjects())
e.ButtonState = GridViewDetailRowButtonState.Hidden;
}
对于经验较少的人,我将快速解释这段代码。为了获取行值,我们将使用传递给事件的参数,该参数包含当前正在处理行的可见索引,并利用该信息检索该行的ID字段的值。下面的代码gvMaster.GetRowValues(e.VisibleIndex, "ID"))
将给出当前行的ID字段的值。然后,我们将检索给定ID的用户类实例,并检查它是否有关联项目。如果此用户未关联任何项目,我们将通过将参数属性ButtonState
设置为hidden来隐藏该行的加号(按钮)。现在,让我们绑定详细网格。每次用户单击加号时,网格将自动生成一个回调,并在服务器端引发详细网格的绑定事件。详细网格中的OnBeforePerformDataSelect
事件是指定当前详细网格应绑定的数据源的正确位置。定义前面提到的事件:<dx:ASPxGridView ID="gvDetail" runat="server" Width="100%"
OnBeforePerformDataSelect="gvDetail_BeforePerformDataSelect" KeyFieldName="ID">
并在该事件中绑定数据:protected void gvDetail_BeforePerformDataSelect(object sender, EventArgs e)
{
ASPxGridView grid = sender as ASPxGridView;
int currentUserID = (int)grid.GetMasterRowKeyValue();
grid.DataSource = Users.Find(u => u.ID == currentUserID).Projects;
}
为了引用正确的对象,我们需要将事件的发送者转换为ASPxGridView
。然后,DevExpress网格将通过GetMasterRowKeyValue()
方法提供帮助,该方法顾名思义,将返回当前定义了详细网格的主网格的键值。有了这个值(基本上是用户ID),我们就可以检索所需的数据,并将其设置为当前详细网格的DataSource
。这样,您的主从详细网格现在应该可以工作了,看起来应该像这样。

如果您的解决方案看起来与我的不完全相同,请不用担心,重要的是它现在能够编译并正确显示数据。您可以看到我不断地为详细网格指出当前的事实。这一点很重要,因为页面上可能有多个详细网格,所以我们总是需要引用正确的对象。在处理详细网格时,请始终考虑这一点。
为详细网格添加上下文菜单
这是一项更复杂的任务,但正如您将看到的,实现它的方法非常简单。首先,在详细网格上添加一个客户端事件ContextMenu
:<dx:ASPxGridView ID="gvDetail" runat="server" Width="100%" OnBeforePerformDataSelect="gvDetail_BeforePerformDataSelect"
KeyFieldName="ID">
<Columns>
<dx:GridViewDataTextColumn FieldName="ID" VisibleIndex="0">
</dx:GridViewDataTextColumn>
<dx:GridViewDataTextColumn FieldName="Name" VisibleIndex="1">
</dx:GridViewDataTextColumn>
<dx:GridViewDataTextColumn FieldName="Status" VisibleIndex="2">
</dx:GridViewDataTextColumn>
</Columns>
<ClientSideEvents ContextMenu="OnContextMenu" />
</dx:ASPxGridView>
然后定义一个包含几个项目的菜单(只需将PopupMenu控件从工具箱拖放到页面上):<dx:ASPxPopupMenu ID="detailContextMenu" runat="server" ClientInstanceName="detailContextMenu">
<Items>
<dx:MenuItem Name="cmdResetTest" Text="Reset Test" ToolTip="Reset Test to New Status">
</dx:MenuItem>
</Items>
</dx:ASPxPopupMenu>
使用DevExpress控件,我们可以在aspx页面中定义客户端(JavaScript)事件。与服务器端一样,它们会在某个事件之后触发。我们需要分配一个函数名,一旦事件触发,我们计划在客户端执行该函数。我们的函数将接收两个参数:第一个是生成事件的控件(发送者),第二个是事件参数。每个事件和控件都有自己的参数。DevExpress控件在客户端功能非常丰富,您可以查阅文档来查看所有可用的客户端事件、函数和属性。在以下参考中,您可以查看ASPxGridView可用的客户端功能。如果您对其他控件感兴趣,只需浏览以Script结尾的感兴趣控件命名空间即可。在页面标题(或单独的.js文件中)定义以下函数:<script type="text/javascript">
function OnContextMenu(s, e) {
if (e.objectType == 'row') {
detailContextMenu.ShowAtPos(ASPxClientUtils.GetEventX(e.htmlEvent), ASPxClientUtils.GetEventY(e.htmlEvent));
}
}
</script>
在此函数中,我们将检查上下文菜单事件是针对网格标题还是网格行触发的。如果是网格行,我们应该弹出上下文菜单。我们可以通过在aspx文件中为该控件定义的ClientInstanceName
来引用客户端的控件,在本例中就是这样做的。每次将DevExpress控件放在页面上时,您将自动获得一个名为ASPxClientUtils
的实用类,其中包含多种方法,可以帮助您减少JS代码。现在,每次右键单击详细网格行时,都会显示您定义的上下文菜单。我们缺少的是用户选择上下文菜单时需要执行的操作。为了实现这一点,我们需要为我的ASPxPopupMenu添加一个客户端事件:<dx:ASPxPopupMenu ID="detailContextMenu" runat="server" ClientInstanceName="detailContextMenu">
<Items>
<dx:MenuItem Name="cmdResetTest" Text="Reset Test" ToolTip="Reset Test to New Status">
</dx:MenuItem>
</Items>
<ClientSideEvents ItemClick="detailContextMenu_ItemClick" />
</dx:ASPxPopupMenu>
还有一些工作要做。由于我们将使用详细网格的回调方法在服务器端处理操作,因此我们需要找到需要更新的正确详细网格。为了实现这一点,我们需要修改我们之前定义的OnContextMenu
方法。<script type="text/javascript">
var currentDetailGrid;
var currentVisibleIndex;
function OnContextMenu(s, e) {
if (e.objectType == 'row') {
currentDetailGrid = s;
currentVisibleIndex = e.index;
detailContextMenu.ShowAtPos(ASPxClientUtils.GetEventX(e.htmlEvent), ASPxClientUtils.GetEventY(e.htmlEvent));
}
}
</script>
在这里,我们保存了对详细网格的引用和当前可见索引(用户右键单击时鼠标所在行的索引),以便我们可以在稍后定义的Popup ItemClick
事件中重用它。<script type="text/javascript">
function detailContextMenu_ItemClick(s, e) {
if (e.item.name == 'cmdResetTest') {
currentDetailGrid.PerformCallback(currentVisibleIndex);
}
}
</script>
一旦选择了菜单项,我们将检查该项目是否正确(如果我们有多个项目并且需要根据所选项目执行不同的操作,这一点很有用),然后请求当前用户正在操作的网格的回调。我们将当前可见索引作为参数传递,以便能够找到我们要对其应用操作的正确元素。在声明此服务器端事件之前,我们需要在aspx文件中指定它:<dx:ASPxGridView ID="gvDetail" runat="server" Width="100%" OnBeforePerformDataSelect="gvDetail_BeforePerformDataSelect"
önCustomCallback="gvDetail_CustomCallback" KeyFieldName="ID">
然后在服务器端管理此事件:protected void gvDetail_CustomCallback(object sender, ASPxGridViewCustomCallbackEventArgs e)
{
ASPxGridView grid = sender as ASPxGridView;
int projectID = (int)grid.GetRowValues(int.Parse(e.Parameters), "ID");
int currentUserID = (int)grid.GetMasterRowKeyValue();
List<Project> projects = Users.Find(u => u.ID == currentUserID).Projects;
projects.Find(p => p.ID == projectID).Status = ProjectStatus.New;
grid.DataSource = projects;
grid.DataBind();
}
和以前一样,为了简单起见,我们将发送者参数转换为名为grid的ASPxGridView变量。然后,我们将检索所选项目的ID。之前在客户端传递给PerformCallback
函数的参数现在会派上用场,因为它将存储查找所需项目所需的数据(通过解析e.Parameters
属性)。下一个需要获取的值是显示关联项目的用户ID。我们可以通过一个方便的服务器端方法GetMasterRowKeyValue()
来获取它,该方法将返回主网格的键值(您还记得我们将KeyFieldName
定义为相关实体的ID属性)。现在,一旦我们有了必要的数据,我们就可以执行所需的操作并重新绑定详细网格。您现在可以为Popup菜单添加不同的操作,并通过传递限定符来管理它们,解析参数并执行不同的操作。您将在我的下一篇博文中看到此技术,敬请关注。以编程方式禁用菜单项
不幸的是,在此示例中,用户可以选择重置非无效状态的项目状态。为了禁用菜单项,如果状态不是“可重置”的,我们需要对代码进行一些更改,至少要将更多信息传递给客户端。在修改JavaScript之前,我们将进行一些考虑。为了禁用菜单中的项,我们需要知道禁用它的条件。我们可以说,当项目状态为“New”时,“Reset”项需要被禁用。我们需要以某种方式将此状态信息传递给客户端。一种方法是将状态信息与键值一起存储在自定义属性中。所有DevExpress控件都可以轻松地从服务器端添加信息,这些信息将被带到客户端并公开。此功能称为自定义属性,您可以在此处找到有关它们的更多信息。我的技术是将Dictionary
元素保存到一个自定义属性中,该属性从JavaScript看来将是一个数组。我所说的一切可能听起来令人困惑,所以让我们看一个实际的例子。首先,我们将订阅OnHtmlRowCreated
事件。修改详细网格如下:<dx:ASPxGridView ID="gvDetail" runat="server" Width="100%" OnBeforePerformDataSelect="gvDetail_BeforePerformDataSelect"
OnCustomCallback="gvDetail_CustomCallback" önHtmlRowCreated="gvDetail_HtmlRowCreated" KeyFieldName="ID">
然后写下以下代码:protected void gvDetail_HtmlRowCreated(object sender, DevExpress.Web.ASPxGridView.ASPxGridViewTableRowEventArgs e)
{
if (e.RowType != GridViewRowType.Data) return;
ProjectStatus status = (ProjectStatus)e.GetValue("Status");
ASPxGridView grid = sender as ASPxGridView;
if (grid.JSProperties.ContainsKey("cpStatus"))
{
Dictionary<int, ProjectStatus> values = (Dictionary<int, ProjectStatus>)grid.JSProperties["cpStatus"];
if (values.ContainsKey(e.VisibleIndex))
{
values[e.VisibleIndex] = status;
}
else
{
values.Add(e.VisibleIndex, status);
}
grid.JSProperties["cpStatus"] = values;
}
else
{
Dictionary<int, ProjectStatus> values = new Dictionary<int, ProjectStatus>();
values.Add(e.VisibleIndex, status);
grid.JSProperties.Add("cpStatus", values);
}
}
这段代码可能看起来很复杂,但它并不复杂。对于将要渲染的每一行,我都会获取其可见索引值,检查我的Dictionary类型变量是否已经有该值。如果没有,我将向我的Dictionary添加一个新项及其状态。如果已更改,我将新的值持久化到网格的自定义JS属性中。这意味着我现在可以轻松地在客户端检索这些信息。修改您的OnContextMenu
函数如下:<script type="text/javascript">
function OnContextMenu(s, e) {
if (e.objectType == 'row') {
var cmdResetTest = detailContextMenu.GetItemByName('cmdResetTest');
cmdResetTest.SetEnabled(s.cpStatus[e.index] != 'New');
currentDetailGrid = s;
currentVisibleIndex = e.index;
detailContextMenu.ShowAtPos(ASPxClientUtils.GetEventX(e.htmlEvent), ASPxClientUtils.GetEventY(e.htmlEvent));
}
}
</script>
我们首先需要检索菜单项,然后根据自定义JS属性值设置enable属性。就这样,尝试您的代码,它应该可以工作。美化外观
为了让您的解决方案看起来像我的,您还需要设置主题和其他一些属性,我将在下面解释。首先是主题。DevExpress控件附带多个主题,请参阅以下网页以获取有关如何将主题部署到您的解决方案的更多详细信息。我通过将必要的文件导入到我的解决方案中并将web.config更改为以下内容来应用Acqua主题(如果不存在,请在system.web部分中添加以下代码):<pages theme="Aqua">
</pages>
我还向两个网格添加了标题面板和标题:<Settings ShowTitlePanel="True" />
<SettingsText Title="Master Grid / Detail for detail grid" />
为了使被单击的行在视觉上有所不同,我还启用了AllowFocusedRow
属性。由于它只在左键单击时设置焦点,所以我还修改了我的JS OnContextMenu
函数,以便在右键单击时也能获得焦点:<script type="text/javascript">
function OnContextMenu(s, e) {
if (e.objectType == 'row') {
s.SetFocusedRowIndex(e.index);
var cmdResetTest = detailContextMenu.GetItemByName('cmdResetTest');
cmdResetTest.SetEnabled(s.cpStatus[e.index] != 'New');
currentDetailGrid = s;
currentVisibleIndex = e.index;
detailContextMenu.ShowAtPos(ASPxClientUtils.GetEventX(e.htmlEvent), ASPxClientUtils.GetEventY(e.htmlEvent));
}
}
</script>
EnableRowHotTrack
也被启用,因此网格会显示热跟踪行(鼠标指针下方的行)。您可以在DevExpress网站上阅读更多关于所有这些属性的信息。下载与源代码
您可以在此处下载我的项目的源代码 here。您可以在此处找到所需控件的试用版 here。