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

ASPxGridView Excel 样式 – 为网格单元格添加注释

starIconstarIconstarIconstarIconstarIcon

5.00/5 (10投票s)

2012 年 10 月 24 日

CPOL

19分钟阅读

viewsIcon

57549

downloadIcon

5405

为网格单元格添加备注并进行视觉标记。

介绍  

我将继续撰写一系列关于 Devexpress 控件的文章。您可以在我之前的文章中找到关于 Devexpress 控件是什么、如何以及在哪里获取它们、版本信息和先决条件等内容的介绍,该文章的地址是:https://codeproject.org.cn/Articles/434866/ASPxGridView-master-detail-data-presentation-with。在这篇文章中,我们将实现一个简单的应用程序,该应用程序将列出信用卡以及每张卡每月花费的金额。我们将能够轻松地添加一张新信用卡并编辑所有月份的支出。此外,我们还可以为每个月添加备注,并直观地看到包含备注的月份。在此示例中,我将使用数据库并创建两个简单的表来存储所有数据。同样,这将是一个简单的示例,并非旨在功能上完全正确。我唯一的目标是向您展示如何与这些控件进行交互以实现特定的功能,在这种情况下,就像 Excel 一样显示、添加和修改备注。

目录

  1. 必要条件与结果
  2. 创建数据库架构
  3. 编写数据服务
  4. 创建页面
  5. 定义 ASPxGridView
  6. 显示带有备注的单元格
  7. 在弹出屏幕中显示备注
  8. 添加、编辑和删除备注
  9. 更改年份
  10. 下载与源代码
  11. 结语

必要条件与结果

附带的项目是用 Visual Studio 2012 编写的。Express 版本应该足够了。您还需要 Microsoft SQL Server 2012(Express 或更高级的版本都可以)。数据库是项目的一部分,将由 VS 自动挂载。您可以通过创建自己的数据库并在 web.config 中更改连接字符串来轻松更改此设置。最后但同样重要的是,您需要在计算机上安装 DevExpress ASP.NET 控件的 12.1.7 版本。如果您有更新的版本,升级项目应该很容易。请参阅这篇文章,了解如何获取 DevExpress 控件以及如何最终升级项目:https://codeproject.org.cn/Articles/434866/ASPxGridView-master-detail-data-presentation-with。最终结果应该与此类似,但还有更多内容,所以请务必阅读完这篇文章。

Final Example

您也可以在此处查看 实时演示。实时版本会将数据保存在会话中,因此每个新会话都会重置您插入的所有数据。因此,它也稍微简化了一些。如果您对代码感兴趣,我可以提供给您,只需在评论中询问。

创建数据库架构

首先,我们将创建一个表来存储我们所有的信用卡。我们将称之为 tbl_CreditCards。它将包含以下列:

tbl_CreditCards

我们将添加的另一个表是 tbl_Imports,其中将存储我们每张卡的、带有备注的所有支出。表的结构如下:

tbl_Imports

我们还需要记住添加外键约束,我们将称之为 FK_tbl_Imports_tbl_CreditCards。这是将执行必要操作的完整 SQL 脚本。

CREATE TABLE [dbo].[tbl_CreditCards]
(
    [Id] INT NOT NULL PRIMARY KEY IDENTITY, 
    [Name] NVARCHAR(50) NOT NULL, 
    [Active] BIT NOT NULL DEFAULT 1
)

CREATE TABLE [dbo].[tbl_Imports]
(
    [Id] INT NOT NULL PRIMARY KEY, 
    [Year] SMALLINT NOT NULL, 
    [Month] TINYINT NOT NULL, 
    [CreditCardID] INT NOT NULL,
    [Import] MONEY NOT NULL, 
    [Note] NVARCHAR(500) NULL,
    CONSTRAINT [FK_tbl_Imports_tbl_CreditCards] FOREIGN KEY ([CreditCardID]) REFERENCES [tbl_CreditCards](Id)
)
我们还将添加一个存储过程,以帮助我们从数据库中检索数据。将需要一个参数,我们将根据该参数检索所有数据并预格式化以便显示。我们需要检索指定年份所有月份的卡名、支出和备注。
/****** Object:  StoredProcedure [dbo].[sp_GetCreditCardImports]    Script Date: 10/21/2012 23:49:13 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:		Mario Majcica
-- Create date: 30-3-2012
-- Description:	Retrieves all the imports per given year
-- =============================================
CREATE PROCEDURE [dbo].[sp_GetCreditCardImports] 
	@YEAR smallint = 0
AS
BEGIN
	SET NOCOUNT ON;

	SELECT DISTINCT tbl_CreditCards.Name, tbl_CreditCards.Id,
        (SELECT tbl_Imports.Import FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 1  AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [1],
        (SELECT tbl_Imports.Import FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 2  AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [2],
        (SELECT tbl_Imports.Import FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 3  AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [3],
        (SELECT tbl_Imports.Import FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 4  AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [4],
        (SELECT tbl_Imports.Import FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 5  AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [5],
        (SELECT tbl_Imports.Import FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 6  AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [6],
        (SELECT tbl_Imports.Import FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 7  AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [7],
        (SELECT tbl_Imports.Import FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 8  AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [8],
        (SELECT tbl_Imports.Import FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 9  AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [9],
        (SELECT tbl_Imports.Import FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 10 AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [10],
        (SELECT tbl_Imports.Import FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 11 AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [11],
        (SELECT tbl_Imports.Import FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 12 AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [12],
        (SELECT tbl_Imports.Note FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 1  AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [N1],
        (SELECT tbl_Imports.Note FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 2  AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [N2],
        (SELECT tbl_Imports.Note FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 3  AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [N3],
        (SELECT tbl_Imports.Note FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 4  AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [N4],
        (SELECT tbl_Imports.Note FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 5  AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [N5],
        (SELECT tbl_Imports.Note FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 6  AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [N6],
        (SELECT tbl_Imports.Note FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 7  AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [N7],
        (SELECT tbl_Imports.Note FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 8  AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [N8],
        (SELECT tbl_Imports.Note FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 9  AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [N9],
        (SELECT tbl_Imports.Note FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 10 AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [N10],
        (SELECT tbl_Imports.Note FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 11 AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [N11],
        (SELECT tbl_Imports.Note FROM tbl_Imports WHERE tbl_Imports.Year = @YEAR AND tbl_Imports.Month = 12 AND tbl_Imports.CreditCardID = tbl_CreditCards.Id) AS [N12]
    FROM tbl_CreditCards LEFT JOIN tbl_Imports ON tbl_CreditCards.Id = tbl_Imports.CreditCardID
END
为了测试我们的存储过程和其他查询,我们将插入一些测试数据。
SET IDENTITY_INSERT [dbo].[tbl_CreditCards] ON
INSERT INTO [dbo].[tbl_CreditCards] ([Id], [Name], [Active]) VALUES (1, N'ABN Amro Gold', 1)
INSERT INTO [dbo].[tbl_CreditCards] ([Id], [Name], [Active]) VALUES (2, N'ABN Amro Visa', 1)
INSERT INTO [dbo].[tbl_CreditCards] ([Id], [Name], [Active]) VALUES (3, N'ING Mastercard', 1)
INSERT INTO [dbo].[tbl_CreditCards] ([Id], [Name], [Active]) VALUES (4, N'Rabobank American Express', 1)
SET IDENTITY_INSERT [dbo].[tbl_CreditCards] OFF

SET IDENTITY_INSERT [dbo].[tbl_Imports] ON
INSERT INTO [dbo].[tbl_Imports] ([Id], [Year], [Month], [CreditCardID], [Import], [Note]) VALUES (1, 2012, 1, 1, 1200, NULL)
INSERT INTO [dbo].[tbl_Imports] ([Id], [Year], [Month], [CreditCardID], [Import], [Note]) VALUES (2, 2012, 2, 1, 200, NULL)
INSERT INTO [dbo].[tbl_Imports] ([Id], [Year], [Month], [CreditCardID], [Import], [Note]) VALUES (3, 2012, 3, 1, 3500, 'Spent too much!')
INSERT INTO [dbo].[tbl_Imports] ([Id], [Year], [Month], [CreditCardID], [Import], [Note]) VALUES (4, 2012, 4, 1, 500, NULL)
INSERT INTO [dbo].[tbl_Imports] ([Id], [Year], [Month], [CreditCardID], [Import], [Note]) VALUES (5, 2012, 5, 1, 700, NULL)
INSERT INTO [dbo].[tbl_Imports] ([Id], [Year], [Month], [CreditCardID], [Import], [Note]) VALUES (6, 2012, 6, 1, 100, NULL)
INSERT INTO [dbo].[tbl_Imports] ([Id], [Year], [Month], [CreditCardID], [Import], [Note]) VALUES (7, 2012, 7, 1, 1700, NULL)
INSERT INTO [dbo].[tbl_Imports] ([Id], [Year], [Month], [CreditCardID], [Import], [Note]) VALUES (8, 2012, 1, 2, 730, NULL)
INSERT INTO [dbo].[tbl_Imports] ([Id], [Year], [Month], [CreditCardID], [Import], [Note]) VALUES (9, 2012, 2, 2, 220, NULL)
INSERT INTO [dbo].[tbl_Imports] ([Id], [Year], [Month], [CreditCardID], [Import], [Note]) VALUES (10, 2012, 3, 2, 800, NULL)
INSERT INTO [dbo].[tbl_Imports] ([Id], [Year], [Month], [CreditCardID], [Import], [Note]) VALUES (11, 2012, 4, 2, 120, NULL)
INSERT INTO [dbo].[tbl_Imports] ([Id], [Year], [Month], [CreditCardID], [Import], [Note]) VALUES (12, 2012, 5, 2, 720, NULL)
INSERT INTO [dbo].[tbl_Imports] ([Id], [Year], [Month], [CreditCardID], [Import], [Note]) VALUES (13, 2012, 6, 2, 190, NULL)
INSERT INTO [dbo].[tbl_Imports] ([Id], [Year], [Month], [CreditCardID], [Import], [Note]) VALUES (14, 2012, 7, 2, 1780, NULL)
SET IDENTITY_INSERT [dbo].[tbl_Imports] OFF

编写数据服务

我们的数据服务将是一个简单的类,它将公开几个静态方法来帮助我们执行所有 CRUD 操作。让我们来定义它们。由于我想让组合框显示数据库中存在的所有年份以及当前年份和次一年,以便用户可以通过这些值进行筛选,因此我将首先准备一个方法来检索和准备这些数据。
/// <summary>
/// Retrives all years from the imports table.
/// Adds if missing the current year and the following year.
/// </summary>
/// <returns>An ordered list of years.</returns>public static SortedDictionary<int, string> GetYears()
{
    SortedDictionary<int,> years = new SortedDictionary<int,>();
    string query = @"SELECT DISTINCT [Year] FROM [tbl_Imports] ORDER BY [Year]";

    using (SqlConnection cn = new SqlConnection(ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString))
    {
        using (SqlCommand cmd = new SqlCommand(query, cn))
        {
            cn.Open();

            using (SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection))
            {
                while (reader.Read())
                {
                    years.Add((int)reader[0], reader[0].ToString());
                }
            }
        }
    }

    int currentYear = DateTime.Now.Year;
    int followingYear = currentYear + 1;

    if (!years.ContainsKey(currentYear))
        years.Add(currentYear, currentYear.ToString());

    if (!years.ContainsKey(followingYear))
        years.Add(followingYear, followingYear.ToString());

    return years;
}
现在我们需要准备一个方法来调用我们的存储过程并返回数据。
/// <summary>
/// Retrives all credit cards imports and notes for the indicated year grouped by month
/// </summary>
/// <param name="year">Accrual Year</param>
/// <returns>Datatable contining the requested data.</returns>
public static DataTable GetAllByDetail(int year)
{
    DataTable table = new DataTable();

    using (SqlConnection cn = new SqlConnection(ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString))
    {
        using (SqlCommand cmd = new SqlCommand("sp_GetCreditCardImports", cn))
        {
            cmd.Parameters.Add("@YEAR", SqlDbType.SmallInt).Value = year;
            cmd.CommandType = CommandType.StoredProcedure;
            cn.Open();

            using (SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection))
            {
                table.Load(reader);
            }
        }
    }

    return table;
}
对于这个第一部分,我们还需要准备管理备注的方法。
public static bool AddNewNote(int creditCardID, int year, byte month, string note)
{
    if (string.IsNullOrEmpty(note))
        return true;

    string query = @"UPDATE tbl_Imports SET [Note] = @note WHERE CreditCardID = @creditCardID AND [MONTH] = @month AND [YEAR] = @year";

    using (SqlConnection cn = new SqlConnection(ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString))
    {
        using (SqlCommand cmd = new SqlCommand(query.ToString(), cn))
        {
            cmd.Parameters.Add("@note", SqlDbType.NVarChar, 500).Value = note;
            cmd.Parameters.Add("@creditCardID", SqlDbType.Int).Value = creditCardID;
            cmd.Parameters.Add("@year", SqlDbType.SmallInt).Value = year;
            cmd.Parameters.Add("@month", SqlDbType.SmallInt).Value = month;

            cn.Open();
            return cmd.ExecuteNonQuery() > 0;
        }
    }
}

public static bool DeleteNote(int creditCardID, int year, byte month)
{
    string query = @"UPDATE tbl_Imports SET [Note] = null WHERE CreditCardID = @creditCardID AND [MONTH] = @month AND [YEAR] = @year";

    using (SqlConnection cn = new SqlConnection(ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString))
    {
        using (SqlCommand cmd = new SqlCommand(query.ToString(), cn))
        {
            cmd.Parameters.Add("@creditCardID", SqlDbType.Int).Value = creditCardID;
            cmd.Parameters.Add("@year", SqlDbType.SmallInt).Value = year;
            cmd.Parameters.Add("@month", SqlDbType.SmallInt).Value = month;

            cn.Open();
            return cmd.ExecuteNonQuery() > 0;
        }
    }
}

public static bool UpdateNote(int creditCardID, int year, byte month, string note)
{
    if (string.IsNullOrEmpty(note))
    {
        return DataService.DeleteNote(creditCardID, year, month);
    }

    string query = @"UPDATE tbl_Imports SET [Note] = @note WHERE CreditCardID = @creditCardID AND [MONTH] = @month AND [YEAR] = @year";

    using (SqlConnection cn = new SqlConnection(ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString))
    {
        using (SqlCommand cmd = new SqlCommand(query.ToString(), cn))
        {
            cmd.Parameters.Add("@creditCardID", SqlDbType.Int).Value = creditCardID;
            cmd.Parameters.Add("@year", SqlDbType.SmallInt).Value = year;
            cmd.Parameters.Add("@month", SqlDbType.SmallInt).Value = month;
            cmd.Parameters.Add("@note", SqlDbType.NVarChar, 500).Value = note;

            cn.Open();
            return cmd.ExecuteNonQuery() > 0;
        }
    }
}

public static string GetNote(int creditCardID, int year, byte month)
{
    string query = @"SELECT [Note] FROM tbl_Imports WHERE CreditCardID = @creditCardID AND [MONTH] = @month AND [YEAR] = @year";

    using (SqlConnection cn = new SqlConnection(ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString))
    {
        using (SqlCommand cmd = new SqlCommand(query.ToString(), cn))
        {
            cmd.Parameters.Add("@creditCardID", SqlDbType.Int).Value = creditCardID;
            cmd.Parameters.Add("@year", SqlDbType.SmallInt).Value = year;
            cmd.Parameters.Add("@month", SqlDbType.SmallInt, 500).Value = month;

            cn.Open();
            return (string)cmd.ExecuteScalar();
        }
    }
}
现在我们可以开始编写我们的页面了。

创建页面

在我们的站点默认页面中,我们将添加一个 ASPxLabel 和一个 ASPxComboBox。前者将显示“Selected Year”文本,后者将列出我们将通过 GetYears 方法检索到的所有年份。请原谅我使用表格来设置布局,div 元素总是让我很困扰,尤其是在垂直对齐方面。我还将添加一个 ObjectDataSource 用于绑定。
<table border="0">
    <tr style="vertical-align: middle;">
        <td style="padding-right: 5px;">
            <dx:ASPxLabel ID="lblSelectedYear" runat="server" Text="Selected Year:" AssociatedControlID="cbYear"></dx:ASPxLabel>
        </td>
        <td>
            <dx:ASPxComboBox ID="cbYears" runat="server" ValueType="System.Int32" TextField="Value" ValueField="Key" Width="70px" DataSourceID="odsYears" OnDataBound="cbYears_DataBound">
                <ClientSideEvents SelectedIndexChanged="cbYears_SelectedIndexChanged" />
            </dx:ASPxComboBox>
        </td>
    </tr>
</table>
<asp:objectdatasource id="odsYears" runat="server" selectmethod="GetYears" typename="ExcelNote.App_Code.DataService" />
正如您所见,我将 GetYears 方法用作我的 SelectMethod,并将 TextField 和 ValueField 设置为正确的名称,以便绑定到 Dictionary 对象。对于 ASPxComboBox,我指定了一个客户端事件,之后我将创建一个回调来反映更改。现在代码将无法正确运行,因为我们的 JavaScript 文件中没有指定客户端事件,但请放心,一旦我们指定了我们的网格,我们将编写必要的内容。最后一件事是定义服务器端 DataBound 事件,我们将在此事件中将当前年份设置为 ASPxComboBox 中的选定项。这是缺失的代码。
protected void cbYears_DataBound(object sender, EventArgs e)
{
    // Select current year as default year to show
    cbYears.SelectedIndex = cbYears.Items.IndexOfValue(DateTime.Now.Year);
}

现在,如果您运行您的解决方案,应该会看到类似这样的内容:Populated Combo

现在让我们定义网格。

定义 ASPxGridView

最长、最耗时的工作是定义列。我将向您展示网格的完整代码,然后讨论我使用的所有属性和事件。
<dx:ASPxGridView ID="gvImports" runat="server" Width="100%" KeyFieldName="Id" OnCustomUnboundColumnData="gvImports_CustomUnboundColumnData" OnDataBinding="gvImports_DataBinding" AutoGenerateColumns="False">
        <Columns>
            <dx:GridViewDataTextColumn FieldName="Name" Caption="Credit Card" VisibleIndex="0">
                <EditCellStyle>
                    <Paddings PaddingLeft="8px" />
                </EditCellStyle>
                <EditItemTemplate>
                    <%# Eval("Name") %>
                </EditItemTemplate>
            </dx:GridViewDataTextColumn>
            <dx:GridViewDataTextColumn FieldName="1" Caption="Jan" ToolTip="January" VisibleIndex="1">
                <PropertiesTextEdit DisplayFormatString="N2" NullDisplayText="-">
                </PropertiesTextEdit>
            </dx:GridViewDataTextColumn>
            <dx:GridViewDataTextColumn FieldName="2" Caption="Feb" ToolTip="February" VisibleIndex="2">
                <PropertiesTextEdit DisplayFormatString="N2" NullDisplayText="-">
                </PropertiesTextEdit>
            </dx:GridViewDataTextColumn>
            <dx:GridViewDataTextColumn FieldName="3" Caption="Mar" ToolTip="March" VisibleIndex="3">
                <PropertiesTextEdit DisplayFormatString="N2" NullDisplayText="-">
                </PropertiesTextEdit>
            </dx:GridViewDataTextColumn>
            <dx:GridViewDataTextColumn FieldName="4" Caption="Apr" ToolTip="April" VisibleIndex="4">
                <PropertiesTextEdit DisplayFormatString="N2" NullDisplayText="-">
                </PropertiesTextEdit>
            </dx:GridViewDataTextColumn>
            <dx:GridViewDataTextColumn FieldName="5" Caption="May" ToolTip="May" VisibleIndex="5">
                <PropertiesTextEdit DisplayFormatString="N2" NullDisplayText="-">
                </PropertiesTextEdit>
            </dx:GridViewDataTextColumn>
            <dx:GridViewDataTextColumn FieldName="6" Caption="Jun" ToolTip="June" VisibleIndex="6">
                <PropertiesTextEdit DisplayFormatString="N2" NullDisplayText="-">
                </PropertiesTextEdit>
            </dx:GridViewDataTextColumn>
            <dx:GridViewDataTextColumn FieldName="7" Caption="Jul" ToolTip="July" VisibleIndex="7">
                <PropertiesTextEdit DisplayFormatString="N2" NullDisplayText="-">
                </PropertiesTextEdit>
            </dx:GridViewDataTextColumn>
            <dx:GridViewDataTextColumn FieldName="8" Caption="Aug" ToolTip="August" VisibleIndex="8">
                <PropertiesTextEdit DisplayFormatString="N2" NullDisplayText="-">
                </PropertiesTextEdit>
            </dx:GridViewDataTextColumn>
            <dx:GridViewDataTextColumn FieldName="9" Caption="Sep" ToolTip="September" VisibleIndex="9">
                <PropertiesTextEdit DisplayFormatString="N2" NullDisplayText="-">
                </PropertiesTextEdit>
            </dx:GridViewDataTextColumn>
            <dx:GridViewDataTextColumn FieldName="10" Caption="Oct" ToolTip="October" VisibleIndex="10">
                <PropertiesTextEdit DisplayFormatString="N2" NullDisplayText="-">
                </PropertiesTextEdit>
            </dx:GridViewDataTextColumn>
            <dx:GridViewDataTextColumn FieldName="11" Caption="Nov" ToolTip="November" VisibleIndex="11">
                <PropertiesTextEdit DisplayFormatString="N2" NullDisplayText="-">
                </PropertiesTextEdit>
            </dx:GridViewDataTextColumn>
            <dx:GridViewDataTextColumn FieldName="12" Caption="Dec" ToolTip="December" VisibleIndex="12">
                <PropertiesTextEdit DisplayFormatString="N2" NullDisplayText="-">
                </PropertiesTextEdit>
            </dx:GridViewDataTextColumn>
            <dx:GridViewDataTextColumn Caption="Total" FieldName="Total" UnboundType="Decimal">
                <PropertiesTextEdit DisplayFormatString="N2" NullDisplayText="-">
                </PropertiesTextEdit>
                <EditItemTemplate>
                </EditItemTemplate>
            </dx:GridViewDataTextColumn>
        </Columns>
        <TotalSummary>
            <dx:ASPxSummaryItem ShowInColumn="1" SummaryType="Sum" FieldName="1" DisplayFormat="N2" />
            <dx:ASPxSummaryItem ShowInColumn="2" SummaryType="Sum" FieldName="2" DisplayFormat="N2" />
            <dx:ASPxSummaryItem ShowInColumn="3" SummaryType="Sum" FieldName="3" DisplayFormat="N2" />
            <dx:ASPxSummaryItem ShowInColumn="4" SummaryType="Sum" FieldName="4" DisplayFormat="N2" />
            <dx:ASPxSummaryItem ShowInColumn="5" SummaryType="Sum" FieldName="5" DisplayFormat="N2" />
            <dx:ASPxSummaryItem ShowInColumn="6" SummaryType="Sum" FieldName="6" DisplayFormat="N2" />
            <dx:ASPxSummaryItem ShowInColumn="7" SummaryType="Sum" FieldName="7" DisplayFormat="N2" />
            <dx:ASPxSummaryItem ShowInColumn="8" SummaryType="Sum" FieldName="8" DisplayFormat="N2" />
            <dx:ASPxSummaryItem ShowInColumn="9" SummaryType="Sum" FieldName="9" DisplayFormat="N2" />
            <dx:ASPxSummaryItem ShowInColumn="10" SummaryType="Sum" FieldName="10" DisplayFormat="N2" />
            <dx:ASPxSummaryItem ShowInColumn="11" SummaryType="Sum" FieldName="11" DisplayFormat="N2" />
            <dx:ASPxSummaryItem ShowInColumn="12" SummaryType="Sum" FieldName="12" DisplayFormat="N2" />
            <dx:ASPxSummaryItem ShowInColumn="Total" SummaryType="Sum" FieldName="Total" DisplayFormat="N2" />
        </TotalSummary>
        <SettingsBehavior AllowDragDrop="False" AllowSort="False" EnableRowHotTrack="True"
            AllowSelectSingleRowOnly="True" />
        <SettingsPager Mode="ShowAllRecords">
        </SettingsPager>
        <SettingsEditing Mode="Inline" />
        <Settings ShowFooter="True" ShowTitlePanel="True" />
        <SettingsText Title="Credit Cards Year Overview" />
        <Styles>
            <Header HorizontalAlign="Center" VerticalAlign="Middle">
            </Header>
            <Footer Font-Bold="True">
            </Footer>
        </Styles>
    </dx:ASPxGridView>
如您所见,最长、最繁琐的部分是定义列。我们定义了一个卡片名称列、每个月的列以及一个显示每张卡总计的列。总计列是未绑定的,我们将 CustomeUnboundColumnData 事件中执行该列的计算。正如您所能想象的,FieldName 属性是绑定 DataTable 中列的名称,Caption 是列标题文本,ToolTip 则是列标题的工具提示。DisplayFormatString 是格式字符串选项(本例中的 N2 将值格式化为整数和小数位数、千位分隔符以及带可选负号的小数分隔符,小数精度为两位),NullDisplayText(当没有提供数据时)设置为斜杠字符以达到样式目的。对于卡片名称和总计列,我们也指定了当行处于编辑状态时应该显示什么。ASPxGridView 的 TotalSummary 功能用于显示一个额外行以及每个指定列的总计。通过添加 ASPxSummaryItem,我们指定了需要考虑的列(通过 ShowInColumn 属性将我们的汇总结果定位在指定的列中)、应该执行的 SummaryType(此处为 Sum)以及计算所基于的字段。我们设置了一些网格属性,所以让我们检查这些设置的行为。SettingsPager mode ShowAllRecords 基本禁用了分页器,而 SettingsEditing mode Inline 将编辑模式设置为内联。我们还决定显示页脚(对于 Summary 行是必需的)并显示标题面板,网格顶部的一个额外行,用于显示一个特定的标签,例如网格中显示的数据的含义,我们稍后在 SettingsText Title 属性中将其设置为“Credit Cards Year Overview”。此外,我们还禁用了列的拖放(ASPxGridView 的可重排列功能,在本例中不希望如此),禁用了开箱即用的排序功能,并启用了行视觉跟踪(EnableRowHotTrack 使鼠标光标下的行高亮显示)。为了对我们的总计列执行自定义计算,我们定义了 CustomUnboundColumnData 事件。这是它的服务器端实现。
protected void gvImports_CustomUnboundColumnData(object sender, DevExpress.Web.ASPxGridView.ASPxGridViewColumnDataEventArgs e)
{
    if (e.Column.FieldName == "Total")
    {
        e.Value = Convert.ToDecimal(e.GetListSourceFieldValue("1") == DBNull.Value ? 0 : e.GetListSourceFieldValue("1")) +
                    Convert.ToDecimal(e.GetListSourceFieldValue("2") == DBNull.Value ? 0 : e.GetListSourceFieldValue("2")) +
                    Convert.ToDecimal(e.GetListSourceFieldValue("3") == DBNull.Value ? 0 : e.GetListSourceFieldValue("3")) +
                    Convert.ToDecimal(e.GetListSourceFieldValue("4") == DBNull.Value ? 0 : e.GetListSourceFieldValue("4")) +
                    Convert.ToDecimal(e.GetListSourceFieldValue("5") == DBNull.Value ? 0 : e.GetListSourceFieldValue("5")) +
                    Convert.ToDecimal(e.GetListSourceFieldValue("6") == DBNull.Value ? 0 : e.GetListSourceFieldValue("6")) +
                    Convert.ToDecimal(e.GetListSourceFieldValue("7") == DBNull.Value ? 0 : e.GetListSourceFieldValue("7")) +
                    Convert.ToDecimal(e.GetListSourceFieldValue("8") == DBNull.Value ? 0 : e.GetListSourceFieldValue("8")) +
                    Convert.ToDecimal(e.GetListSourceFieldValue("9") == DBNull.Value ? 0 : e.GetListSourceFieldValue("9")) +
                    Convert.ToDecimal(e.GetListSourceFieldValue("10") == DBNull.Value ? 0 : e.GetListSourceFieldValue("10")) +
                    Convert.ToDecimal(e.GetListSourceFieldValue("11") == DBNull.Value ? 0 : e.GetListSourceFieldValue("11")) +
                    Convert.ToDecimal(e.GetListSourceFieldValue("12") == DBNull.Value ? 0 : e.GetListSourceFieldValue("12"));
    }
}
事件参数通过使当前正在处理的列名可供我们使用来帮助我们,因此只有当他正在处理期望的列时,我们将该列的行值分配给所有月份列值的总和。如果 column value 是 DBNull.Value,则使用零,否则将 column value 转换为 Decimal 然后相加。现在是时候绑定我们的网格并检查结果了。我选择使用 DataBinding 事件来为网格分配正确的源。
protected void gvImports_DataBinding(object sender, EventArgs e)
{
    int yearToBind = DateTime.Now.Year;

    if (cbYears.SelectedItem != null)
        yearToBind = Convert.ToInt32(cbYears.SelectedItem.Value);

    gvImports.DataSource = DataService.GetAllByDetail(yearToBind);
}
在将设计的​​方法指定给网格的 DataSource 之前,我会检查参数,如果由于任何原因未在 ComboBox 中指定,我将将其设置为当前年份。实际的绑定方法将在 Page_Load 事件中仅在首次显示时调用(而不是在回发时)。
protected void Page_Load(object sender, EventArgs e)
{
    if (!Page.IsPostBack)
    {
        gvImports.DataBind();
    }
}

现在我们可以运行我们的代码并检查结果。 Grid After First Binding

对于相对较小的努力来说,这是一个很棒的结果,不是吗?

显示带有备注的单元格

现在的想法是为包含备注的每个单元格设置一个特定的背景图像,这样用户一眼就能理解该支出有一个备注。为了动态设置单元格的背景,我们需要在我们的网格上声明 HtmlDataCellPrepared。别忘了在 ASPX 文件中设置事件(OnHtmlDataCellPrepared="gvImports_HtmlDataCellPrepared"),并在您的页面中设置以下方法:
protected void gvImports_HtmlDataCellPrepared(object sender, DevExpress.Web.ASPxGridView.ASPxGridViewTableDataCellEventArgs e)
{
    if (e.DataColumn.FieldName == "Name" || e.DataColumn.FieldName == "Total" || e.CellValue == DBNull.Value)
    {
        return;
    }

    object note = e.GetValue(string.Format("N{0}", e.DataColumn.FieldName));

    if (note != DBNull.Value && note != null)
    {
        e.Cell.Style.Add("background-image", ResolveUrl("~//images//yellow-triangle.png"));
        e.Cell.Style.Add("background-repeat", "no-repeat");
    }
}
代码的第一行是一个快捷方式,它表示如果正在处理的单元格在 Name 或 Total 列中(不能包含备注的列),或者如果 CellValue 为空,我们可以停止处理它。由于我们的 DataSource 包含包含备注的额外列,列的名称是 N 加上其关联的月份列的编号,我们可以通过检查当前处理项的列并请求 GetValue 方法中的备注值来轻松确定列的名称,我们将在其中传递我们要查找的备注列的名称(例如,如果我们正在处理名为 1 的一月列,我们将在当前处理的行中查找列 N1 的值,该列最终将包含备注。现在,如果备注不是 DBNull.Value 并且备注不是 null,我们将添加正在处理的单元格样式属性 background-image 和 background-repeat 到所需值,第一个是我们要的图像,第二个是重复行为(在本例中不应重复)。结果是所有包含备注的单元格都有不同的背景,并且用户可以快速发现它们。

在弹出屏幕中显示备注

到目前为止,我们只显示包含一些备注的单元格,如果能添加一个显示、添加和删除它们的方法会很好。让我们先从显示它们开始。显示备注将通过将鼠标悬停在包含备注的单元格上,然后在给定时间后显示带有备注文本的 div 来实现。我们将通过以下方式实现这一点。首先,我们将定义一个窗口来显示备注本身,并将使用 ASPxPopupControl 来实现此目的:
<dx:ASPxPopupControl ID="PopUpNote" runat="server" ClientInstanceName="PopUpNote"
    HeaderText="Note:" PopupHorizontalAlign="OutsideRight" PopupVerticalAlign="Middle"
    AppearAfter="0" DisappearAfter="0" ShowCloseButton="False" ShowLoadingPanelImage="False">
    <ContentCollection>
        <dx:PopupControlContentControl ID="PopupControlContentControl1" 
               runat="server" SupportsDisabledAttribute="True">
            <dx:ASPxLabel ID="lblNote" runat="server" 
              ClientInstanceName="lblNote" Text="" EncodeHtml="True">
            </dx:ASPxLabel>
        </dx:PopupControlContentControl>
    </ContentCollection>
</dx:ASPxPopupControl>
好吧,大多数属性都是不言自明的,我只是添加了一个 ASPxPopupControl,并在其内容中添加了一个简单的 ASPxLabel 控件。请注意,我为 ASPxPopupControl 控件分配了一个特定的客户端实例名称,以便于在客户端调用方法和属性。现在我们需要定义 JavaScript 函数,这些函数将在 onmouseover 和 onmouseout 客户端事件中分配给每个包含备注的单元格。
var reservePopup = false;

function ClosePopup() {
    reservePopup = false;

    if (PopUpNote.IsVisible()) {
        PopUpNote.Hide();
    }
}

function ShowPopup(note, elementID) {
    lblNote.SetText(note);

    ClosePopup();

    reservePopup = true;

    setTimeout(function () {
        if (reservePopup) {
            PopUpNote.ShowAtElementByID(elementID);
        }
    }, 1000);
}
这段代码可能看起来有点复杂和棘手,所以我将尝试解释一下。当鼠标悬停在感兴趣的单元格上时,将执行 ShowPopup 方法。我们将设置我们刚添加到 ASPxPopupControl 中的标签控件的文本,并关闭任何可能打开的弹出窗口(以防万一)。然后将 reservPopup 标志设置为 true(这用于控制超时,如果鼠标移出单元格,此标志将设置为 false,这意味着如果已请求显示弹出窗口但超时尚未过期,则弹出窗口不会显示,基本上,如果您比给定的超时时间快,则在窗口上触发 Show 函数),并安排一个函数在 1000 毫秒后执行。此函数将检查是否应显示带有备注的弹出窗口,如果显示,它将调用我们的 ASPxPopupControl 的 ShowAtElementByID 方法。现在我们缺少的是将此函数关联到我们的单元格,为了实现这一点,我们需要修改我们的 HtmlDataCellPrepared 事件,如下所示:
protected void gvImports_HtmlDataCellPrepared(object sender, DevExpress.Web.ASPxGridView.ASPxGridViewTableDataCellEventArgs e)
{
    if (e.DataColumn.FieldName == "Name" || e.DataColumn.FieldName == "Total" || e.CellValue == DBNull.Value)
    {
        return;
    }

    object note = e.GetValue(string.Format("N{0}", e.DataColumn.FieldName));

    if (note != DBNull.Value && note != null)
    {
        e.Cell.Style.Add("background-image", ResolveUrl("~//images//yellow-triangle.png"));
        e.Cell.Style.Add("background-repeat", "no-repeat");

        string htmlEncoded = Server.HtmlEncode(note.ToString()).Replace("\n", "");

        e.Cell.Attributes.Add("onmouseover", 
          string.Format("ShowPopup('{0}','{1}')", htmlEncoded, e.Cell.ClientID));
        e.Cell.Attributes.Add("onmouseout", "ClosePopup()");
    }
}

正如您所看到的,我们添加了三行新代码。第一行我们正在准备我们的备注以使其“HTML 兼容”,方法是将任何特殊字符编码为正确的 HTML 表示形式,并将换行符替换为等效的 HTML(<br /> 在这里换行)。另外,作为 ShowPopup 函数的第二个参数,我们将传递单元格的客户端 ID,以便弹出窗口可以显示在单元格旁边。现在我们可以运行并测试我们的代码。

结果不错!

添加、编辑和删除备注

作为另一项要求,当右键单击任何单元格时显示一个弹出菜单,并提供为此单元格添加、编辑或删除备注的选项将会很好。这很有趣,我们将直接开始工作,定义一个带有必要项的 ASPxPopupMenu。
<dx:ASPxPopupMenu ID="gridMenu" runat="server" ClientInstanceName="GridMenu">
    <ClientSideEvents ItemClick="GridMenu_ItemClick" />
    <Items>
        <dx:MenuItem Name="AddNote" Text="Add note" ToolTip="Add note">
        </dx:MenuItem>
        <dx:MenuItem Name="EditNote" Text="Edit Note" ToolTip="Edit Note">
        </dx:MenuItem>
        <dx:MenuItem Name="RemoveNote" Text="Remove Note" ToolTip="Remove Note">
        </dx:MenuItem>
    </Items>
</dx:ASPxPopupMenu>
在我们的 ASPxPopupMenu 中,我们定义了三个菜单项,每个项对应一个我们将要执行的特定信息。我们还分配了一个菜单的 ClientInstanceName 以及一个客户端事件 ItemClick,其实现稍后将在文本中看到。现在我们需要准备的是用于添加和编辑备注的界面。我们将定义另一个 ASPxPopupControl 控件并定义其内容。
<dx:ASPxPopupControl ID="popupAddNote" ClientInstanceName="popupAddNote" runat="server" AppearAfter="0" DisappearAfter="0"
    Modal="True" PopupHorizontalAlign="Center" PopupVerticalAlign="Middle"
    HeaderText="Add note" Width="270px" OnWindowCallback="popupAddNote_WindowCallback" EnableAnimation="False">
    <ContentCollection>
        <dx:PopupControlContentControl ID="PopupControlContentControl2" runat="server" SupportsDisabledAttribute="True">
            <table style="width: 100%;">
                <tr>
                    <td>
                        <dx:ASPxMemo ID="txtNote" ClientInstanceName="txtNote" runat="server" Height="71px"
                            Width="100%">
                        </dx:ASPxMemo>
                    </td>
                </tr>
            </table>
            <hr />
            <table style="width: 100%;">
                <tr>
                    <td>
                        <dx:ASPxButton ID="btnSaveNote" runat="server" 
                            Text="Save" Width="100px" AutoPostBack="False"
                            UseSubmitBehavior="False" CausesValidation="False">
                            <ClientSideEvents Click="btnSaveNote_Click" />
                        </dx:ASPxButton>
                    </td>
                    <td>
                        <dx:ASPxButton ID="btnCancelNote" runat="server" 
                            Text="Cancel" Width="100px" AutoPostBack="False"
                            UseSubmitBehavior="False" CausesValidation="False">
                            <ClientSideEvents Click="btnCancelNote_Click" />
                        </dx:ASPxButton>
                    </td>
                </tr>
            </table>
        </dx:PopupControlContentControl>
    </ContentCollection>
</dx:ASPxPopupControl>
弹出窗口的设置方式与之前的类似,内容仅包含不同的控件。您会看到我使用了一个 ASPxMemo 控件,它非常类似于多行文本框,以及两个按钮。没有一个按钮会导致验证或在单击时生成回发,并且两者都有与其客户端单击事件关联的客户端事件。让我们先查看 ASPxPopupMenu 客户端事件的实现,然后查看其余部分。
function GridMenu_ItemClick(s, e) {
    switch (e.item.name) {
        case 'AddNote':
            currentIsNoteEdit = false;
            txtNote.SetText('');
            popupAddNote.SetHeaderText('Add Note');
            popupAddNote.ShowAtElement(gvImports.mainElement);
            txtNote.Focus();
            break;
        case 'EditNote':
            currentIsNoteEdit = true;
            txtNote.SetText('');
            popupAddNote.SetHeaderText('Edit Note');
            popupAddNote.ShowAtElement(gvImports.mainElement);
            popupAddNote.PerformCallback(currentVisibleIndex + '|' + currentFieldName);
            break;
        case 'RemoveNote':
            deleteCaller = 'Note';
            popupConfirmDelete.Show();
            break;
    }
}

当单击 GridMenu 中的项时,将执行此客户端代码。我们在 switch 语句中检查单击项的名称,并根据此名称执行指示的操作。在 AddNote 的情况下,我们将 currentIsNoteEdit 标志设置为 false。此标志将用于了解我们是在编辑备注还是在添加新备注(因为我们将对两者使用相同的弹出窗口)。然后我们确保将 ASPxMemo 控件的文本设置为空,并将弹出控件的标题设置为“Add Note”(原因与之前相同,它是共享控件,用于不同的操作)。然后我们显示弹出窗口并将焦点设置在 ASPxMemo 控件上。对于 EditNote,情况类似,除了最后我们对弹出控件执行回调。我们这样做是为了检索备注数据,因为我们无法在客户端轻松获取它们(至少不那么容易,仍然会被 HTML 编码,我们会遇到重新编码相同数据的麻烦)。如果您检查 popupAddNote 的定义,您会发现我们已经声明了 OnWindowCallback 事件,现在让我们检查它的实现。

protected void popupAddNote_WindowCallback(object source, DevExpress.Web.ASPxPopupControl.PopupWindowCallbackArgs e)
{
    string[] values = e.Parameter.Split('|');

    int visibleIndex = int.Parse(values[0]);
    int creditCardID = (int)gvImports.GetRowValues(visibleIndex, "Id");
    int year = (int)cbYears.SelectedItem.Value;
    byte month = byte.Parse(values[1]);

    txtNote.Text = DataService.GetNote(creditCardID, year, month);
    txtNote.Focus();
}
您可以从 JavaScript 调用中推断出,我们正在将两个值作为参数传递给此回调:currentVisibleIndex 和 currentFieldName。第一个不言自明,而第二个是列名,在我们的例子中,它是相关的月份。稍后我将回到这个第二个参数。一旦我在服务器端收到这些数据,我将检索必要的信息并设置相关属性。让我们回到 ASPxPopupMenu 的最后一个项,编写并解释代码。其想法是在删除备注之前显示一个确认对话框。我现在将创建一个可重用的对话框,甚至以后可用于其他实体。
<dx:ASPxPopupControl ID="popupConfirmDelete" ClientInstanceName="popupConfirmDelete"
    runat="server" Modal="True" PopupHorizontalAlign="WindowCenter" PopupVerticalAlign="WindowCenter"
    HeaderText="Confirm delete dialog" Width="300px" CloseAction="CloseButton" EnableAnimation="False">
    <ContentCollection>
        <dx:PopupControlContentControl ID="PopupControlContentControl3" 
              runat="server" SupportsDisabledAttribute="True">
            <table>
                <tr>
                    <td>
                        <img alt="Confirm delete" runat="server" src="~/images/red-question-mark.png" />&nbsp;</td>
                    <td>
                        <dx:ASPxLabel ID="lblConfirmDeleteNoteQuestion" runat="server" Text="Are you sure?"
                            Font-Size="X-Large">
                        </dx:ASPxLabel>
                    </td>
                </tr>
            </table>
            <hr />
            <table>
                <tr>
                    <td>
                        <dx:ASPxButton ID="btnDeleteNote" runat="server" 
                            Text="Delete" Width="100px" AutoPostBack="False"
                            UseSubmitBehavior="False" CausesValidation="False">
                            <ClientSideEvents Click="btnDelete_Click" />
                        </dx:ASPxButton>
                    </td>
                    <td>
                        <dx:ASPxButton ID="btnCancelDeleteNote" runat="server" Text="Cancel" Width="100px"
                            AutoPostBack="False" UseSubmitBehavior="False" CausesValidation="False">
                            <ClientSideEvents Click="btnCancelDelete_Click" />
                        </dx:ASPxButton>
                    </td>
                </tr>
            </table>
        </dx:PopupControlContentControl>
    </ContentCollection>
</dx:ASPxPopupControl>
如前所述,我们定义了一个 ASPxPopupControl 并创建了应显示的内容。我们有两个按钮,一个用于取消此操作,另一个用于确认此操作。两者都没有分配服务器端事件或操作,所有操作都在客户端单击时通过预期的事件进行。这些事件的定义如下:
function btnCancelDelete_Click(s, e) { popupConfirmDelete.Hide(); }

function btnDelete_Click(s, e) {
    popupConfirmDelete.Hide();

    if (deleteCaller == 'Note')
        gvImports.PerformCallback('DeleteNote|' + currentVisibleIndex + '|' + currentFieldName);
}
第一个方法不言自明,在取消单击时调用弹出控件的 Hide 方法。第二个方法也首先隐藏弹出窗口,然后为特定的调用者执行一个回调,其中参数是要执行的命令、选定项的可见索引和列名。这与其他所有命令一样,在网格的 CustomCallback 事件中执行。这是我使用的代码:
protected void gvImports_CustomCallback(object sender, DevExpress.Web.ASPxGridView.ASPxGridViewCustomCallbackEventArgs e)
{
    // if no parameter is passed from cliend side, do no process
    if (string.IsNullOrEmpty(e.Parameters))
        return;

    int year = (int)cbYears.SelectedItem.Value;

    string[] values = e.Parameters.Split('|');
    string command = values[0];
    int visibleIndex = 0;
    int creditCardID = 0;

    if (values.Length > 1)
    {
        visibleIndex = int.Parse(values[1]);
        if (command != "ChangeYear")
            creditCardID = (int)gvImports.GetRowValues(visibleIndex, "Id");
    }

    byte month = 0;

    if (values.Length > 2)
        month = byte.Parse(values[2]);

    switch (command)
    {
        case "AddNote":
            DataService.AddNewNote(creditCardID, year, month, txtNote.Text);
            break;
        case "DeleteNote":
            DataService.DeleteNote(creditCardID, year, month);
            break;
        case "EditNote":
            DataService.UpdateNote(creditCardID, year, month, txtNote.Text);
            break;
        case "ChangeYear":
            gvImports.DataSource = DataService.GetAllByDetail(Convert.ToInt32(values[1]));
            break;
    }

    gvImports.DataBind();
}
如果未指定参数,则忽略回调。根据我约定的分隔符拆分字符串,以分隔字符串中的不同值。如果检索到的数组包含多个元素,则同样根据我的约定,第二个参数表示可见索引值,而第一个值表示要执行的操作,除非是 ChangeYear。在这种情况下,第二个值表示网格应绑定的年份。如果数组包含两个以上的值,则第三个值表示月份,这正是我们之前传递的 currentFieldName。现在,一旦我拥有所有数据,我就可以检查命令并执行所需的操作。所有到数据库的调用都在开始时编写好了,所以我不必做更多的事情,只需调用 DataService 的特定方法并将解码后的参数传递过去。简单吗?至少干净(我希望如此)。似乎一切都已连接好,但我们还没有任何东西可以弹出菜单项。为了实现这一点,我们需要修改我们的老熟人,HtmlDataCellPrepared 事件。我们将对其进行如下修改:
protected void gvImports_HtmlDataCellPrepared(object sender, DevExpress.Web.ASPxGridView.ASPxGridViewTableDataCellEventArgs e)
{
    if (e.DataColumn.FieldName == "Name" || 
        e.DataColumn.FieldName == "Total" || e.CellValue == DBNull.Value)
    {
        e.Cell.Attributes.Add("oncontextmenu", string.Format("OnCellContextMenu(
          event, {0}, '{1}', false, false)", e.VisibleIndex, e.DataColumn.FieldName));
        return;
    }

    object note = e.GetValue(string.Format("N{0}", e.DataColumn.FieldName));

    if (note != DBNull.Value && note != null)
    {
        e.Cell.Style.Add("background-image", ResolveUrl("~//images//yellow-triangle.png"));
        e.Cell.Style.Add("background-repeat", "no-repeat");

        string htmlEncoded = Server.HtmlEncode(note.ToString()).Replace("\n", "
");

        e.Cell.Attributes.Add("onmouseover", string.Format("ShowPopup('{0}','{1}')", 
          htmlEncoded, e.Cell.ClientID));
        e.Cell.Attributes.Add("onmouseout", "ClosePopup()");
        e.Cell.Attributes.Add("oncontextmenu", string.Format("OnCellContextMenu(
          event, {0}, '{1}', true, true)", e.VisibleIndex, e.DataColumn.FieldName));
    }
    else
    {
        e.Cell.Attributes.Add("oncontextmenu", string.Format("OnCellContextMenu(event, {0}, '{1}', false, true)", e.VisibleIndex, e.DataColumn.FieldName));
    }
}
正如您所注意到的,我在单元格上添加了一个新属性 oncontextmenu,并将其关联到一个客户端函数 OnCellContextMenu。我们将指定的这个客户端函数需要几个参数:事件、行的可见索引、列名、是否包含备注的指示,以及最后一个单元格是否包含值的指示。所有这些信息将在我们决定显示什么时以及稍后获取执行操作所需的所有数据时非常有用。我们在三个不同的位置为三种不同类型的单元格设置此属性。第一个是对于没有值且不应显示上下文菜单的单元格。我们将在此端处理,但这对于您将在我的下一篇博文中看到的未来改进是必要的。然后我们将它分配给所有包含备注的单元格以及所有不包含备注的单元格,但参数不同。客户端函数如下所示。
function OnCellContextMenu(e, visibleIndex, fieldName, hasNote, hasValue) {
    currentVisibleIndex = visibleIndex;
    currentFieldName = fieldName;

    ShowMenuItem('AddNote', !hasNote && hasValue);
    ShowMenuItem('EditNote', hasNote);
    ShowMenuItem('RemoveNote', hasNote);

    var currentEvent = (window.event) ? window.event : e;

    ASPxClientUtils.PreventEventAndBubble(currentEvent);

    gvImports.SetFocusedRowIndex(visibleIndex);

    if (hasValue)
        GridMenu.ShowAtPos(ASPxClientUtils.GetEventX(currentEvent), ASPxClientUtils.GetEventY(currentEvent));
}

function ShowMenuItem(itemName, enabled) { GridMenu.GetItemByName(itemName).SetVisible(enabled); }

好吧,我们正在持久化可见索引和字段名到全局可访问的变量中,这样我们就可以稍后使用它们。现在我们将根据实际可以执行的操作来显示或隐藏不同的菜单项。如果单元格已包含备注,我们可以修改或删除该备注;否则,如果单元格有值但没有备注,我们只能添加新备注。这基本上解释了接下来的三行代码,因为它们将这些转换为代码。现在我们将确定窗口事件,因为它在不同浏览器上的处理方式有所不同,然后使用 DevExpress 客户端框架中提供的辅助方法来阻止此事件和冒泡。此事件确定对于确保弹出菜单在当今所有主要浏览器上正常工作是必需的。最后一个重要的事情是显示菜单本身。我们仅在有值的单元格上显示菜单,并通过调用 ShowAtPos 方法并再次通过 DevExpress 框架助手计算正确的坐标来实现。就是这样!让我们运行我们的示例并进行测试。当您尝试添加新备注时,您应该会看到以下屏幕。

Final Add Note

更改年份

我们几乎忘记的一件事是设置将更改所选年份的客户端事件代码。最初,我们仅在客户端定义了事件,但实际上没有实现。由于服务器端代码将管理“ChangeYear”参数,我们只需要执行正确的​​回调。
function cbYears_SelectedIndexChanged(s, e) { gvImports.PerformCallback('ChangeYear|' + s.GetValue()); }
正如您在其他场合看到的那样,我们正在对 ASPxGridView 进行回调,并传递命令参数和新选择的值。

下载与源代码

您可以在此处找到我的项目源代码下载:此处。您可以在此处找到所需控件的试用版:此处

结语

第一部分就到这里,如果您有任何问题,请随时发表评论。我将使用这个例子作为后续文章的基础,我将在其中通过引入内联编辑、信用卡上的 CRUD 操作、导出等来扩展功能,敬请期待!

更新

最近,这个项目的扩展版本已发布。如果您对此感兴趣,可以访问以下地址阅读:ASPxGridView Excel 风格 - 扩展功能[^]。
© . All rights reserved.