ASPxGridView Excel style – Extending functionality

Introduction

In this post we will continue adding functionality on our previous example. If you missed my previous post you can get it here: ASPxGridView Excel style – Adding notes to grid cells. The main goal for this time is to add inline editing functionality to the grid, in order to be able to update the imports and manage adding, editing and removing credit cards. However we will add also a couple more enhancements in order to show the potential of the grid and offer benefits to the end users. I will not go in the detail about the environment and developing requirements, if interested jump op to my other posts.

Table of contents

  1. The necessary and the result
  2. Creating database schema
  3. Updating our DataService
  4. In-line editing
  5. Managing Credit Cards
  6. Marking the values in the grid
  7. Exporting the data
  8. Downloads and the source code
  9. Final word

The necessary and the result

The attached project is written in Visual Studio 2012. The express version should be enough. Also you will need a Microsoft SQL Server 2012, Express or and more advanced version will be just fine. The database is part of the project and it will be automatically mounted by VS. You can change this easily be creating your own database and changing the connection string in the web config. Last but not least you will need the 12.2.5 version of DevExpress ASP.NET controls installed on your machine. If you do have a more recent version, upgrading the project should be easy. Check this post about how to get the DevExpress controls and on how to eventually upgrade the project http://blog.majcica.com/2012/08/05/aspxgridview-master-detail-data-presentation-with-context-menus/.

At the end the result should be similar to this, however there is much more, so check this post till the end.
Final Example

You can check it also LIVE. Live version persist the data in the session so each new session will reset all the data you have inserted. Also because of this, it is a bit simplified. If you are interested in the code, I can provide it to you, just ask in comments.

Updating our DataService

As first we will add a couple of new methods in our DataService class. We will write down methods that will allow us to update, add and remove the credit card entities.

public static bool AddNewCreditCard(string creditCardName)
{
    string query = @"INSERT INTO tbl_CreditCards ([Name]) VALUES (@Name)";

    using (SqlConnection cn = new SqlConnection(ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString))
    {
        using (SqlCommand cmd = new SqlCommand(query.ToString(), cn))
        {
            cmd.Parameters.Add("@Name", SqlDbType.NVarChar).Value = creditCardName;

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

public static bool UpdateCreditCard(int creditCardId, string creditCardName)
{
    string query = @"UPDATE tbl_CreditCards SET [Name] = @Name WHERE Id = @creditCardId";

    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("@Name", SqlDbType.NVarChar).Value = creditCardName;

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

public static string GetCreditCard(int creditCardId)
{
    string query = @"SELECT [Name] FROM tbl_CreditCards WHERE Id = @creditCardId";

    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;

            cn.Open();
            return (string)cmd.ExecuteScalar();
        }
    }
}

public static bool RemoveCreditCard(int creditCardId)
{
    string query = @"UPDATE tbl_CreditCards SET [Active] = 0 WHERE Id = @creditCardId";

    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;

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

This code is pretty straight forward. The only exception is the RemoveCreditCard method, which actually doesn’t delete the item and in cascade the related items in the tbl_Import, but only flags the credit card as inactive. In order to make this thing actually work, we need to modify the stored procedure that we defined in previous post sp_GetCreditCardImports.
I will not waste space in this post so I will show you only what you would need to add.

...
WHERE tbl_CreditCards.Active = 1
...

Now is the turn of writing down methods that will Add, Update and Remove imports.

public static bool RemoveImport(int creditCardId, int year, byte month)
{
    string query = @"DELETE FROM tbl_Imports WHERE CreditCardId = @creditCardId AND [Year] = @year AND [Month] = @month";

    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.Int).Value = year;
            cmd.Parameters.Add("@month", SqlDbType.SmallInt).Value = month;

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

public static bool AddImport(int creditCardId, int year, byte month, decimal import)
{
    string query = @"INSERT INTO tbl_Imports ([CreditCardId], [Year], [Month], [Import])
                VALUES (@creditCardId, @year, @month, @import)";

    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.Int).Value = year;
            cmd.Parameters.Add("@month", SqlDbType.SmallInt).Value = month;
            cmd.Parameters.Add("@import", SqlDbType.Money).Value = import;

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

public static bool UpdateImport(int creditCardId, int year, byte month, decimal import)
{
    string query = @"UPDATE tbl_Imports SET [Import] = @import
                WHERE CreditCardId = @creditCardId AND [Year] = @year AND [Month] = @month";

    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.Int).Value = year;
            cmd.Parameters.Add("@month", SqlDbType.SmallInt).Value = month;
            cmd.Parameters.Add("@import", SqlDbType.Money).Value = import;

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

This is it. No more code is necessary for managing data and we can now comfortably start with the interface.

In-line editing

Editing the ASPxGridView is very easy. There are several methods for achieving the same result. If you are interested in more you can check this Grid Editing – Edit Modes
A rich set of editors for different column types allows column values to be modified with ease. Still there are a couple of tricks that we will see in the following lines.
In order to start editing a certain row, we can choose the way we are going to start this action. At example you can add a Command Button in the grid itself or simply start editing let’s say on a row double click. There are also many other modes, you have rich client and server side methods in order to start editing one or more rows. Still I will let you discover them and invent new ones. We are going to make our row editable on double click and in order to achieve that we will write down a function (in JavaScript) that is going to be associated to the grids client side event called RowDblClick. Let’s see how this is done.
At first we will add in our ASPX file, inside the grid definition a new section that is called ClientSideEvents (as we already did for other controls) and it will look like:

<ClientSideEvents RowDblClick="gvPrevisions_RowDblClick" />

Then we will write down the JavaScript method itself:

function gvPrevisions_RowDblClick(s, e) { s.StartEditRow(e.visibleIndex); }

It is that simple! Just call StartEditRow JavaScript method that is defined on the grid (in this case I’m using s and not the ClientInstanceName in order to access the grid methods, but here is basically the same) and pass the visible index of the row that we intend to put in edit mode. Kindly this even will provide us the visible index via the function parameter which will indicate the row that user double clicked. If you run the example right now and double click on a row, you should see something similar to this:
Grid in Edit Mode
Even if this looks nice and simple, you maybe noticed that the fields are not editable. This is because we didn’t binded our grid to a valid and well defined Data Source. Because of this, we have our edit controls in read only state. You can find on Devexpress support web site several issues that are speaking about this behavior and why of it. The good thing is that we can solve this easily. Define the following server side event:

protected void gvImports_CellEditorInitialize(object sender, ASPxGridViewEditorEventArgs e)
{
    if (e.Column.FieldName != "Name" || e.Column.FieldName != "Total")
    {
        e.Editor.ReadOnly = false;

        ASPxTextBox box = e.Editor as ASPxTextBox;
        box.HorizontalAlign = HorizontalAlign.Right;
        box.MaskSettings.Mask = "$ <0..99999g>.<00..99>";
        box.MaskSettings.IncludeLiterals = MaskIncludeLiteralsMode.DecimalSymbol;
        box.ValidationSettings.ErrorDisplayMode = ErrorDisplayMode.ImageWithTooltip;
        box.ValidationSettings.Display = Display.Dynamic;
        box.ClientSideEvents.KeyDown = "gvImports_Cell_KeyDown";

        if (e.Column.FieldName == "1")
            box.Focus();
    }
}

What we defined here is checking if the cells that are going in edit mode are the one we are aiming to put in edit (not the Name and Total columns) and if so we are setting a couple of properties. As first, we are indicating the cell as read only, after that we are casing the control to an ASPxTextBox (which is the right control for these columns) and setting several parameters in order to make the editing look proper. The most important thing is that we are assigning to each of editing cells a client side event KeyDown, in which we will manage the persistence. Let’s see how this important method is defined.

function gvImports_Cell_KeyDown(s, e) {
    switch (e.htmlEvent.keyCode) {
        case 13:
            ASPxClientUtils.PreventEventAndBubble(e.htmlEvent);
            gvImports.UpdateEdit();
            break;
        case 27:
            gvImports.CancelEdit();
            break;
    }
}

If Enter (13) key is pressed meanwhile we are in one of editing cells, we will prevent the defaults for this action and call UpdateEdit method on our grid. In case the user hits Escape key (27) we will just get out of the edit mode by calling CancelEdit method. Once UpdateEdit is called a callback will be generated and the RowUpdating server side method will be executed, in which we will need to handle the persistence of the data. We need to attach the following method to the RowUpdating event:

protected void gvImports_RowUpdating(object sender, ASPxDataUpdatingEventArgs e)
{
    // copy OrderedDictionaries to Hashtables
    // for easier data manipulations
    Hashtable newValues = new Hashtable();
    Hashtable oldValues = new Hashtable();

    foreach (DictionaryEntry item in e.NewValues)
    {
        // if value == 0 then do not insert any value
        newValues.Add(Convert.ToByte(item.Key), (decimal)item.Value == 0 ? null : (decimal?)item.Value);
    }

    foreach (DictionaryEntry item in e.OldValues)
    {
        // transform DbNull to null
        oldValues.Add(Convert.ToByte(item.Key), item.Value == DBNull.Value ? null : (decimal?)item.Value);
    }

    int creditCardID = (int)e.Keys[0];
    int year = (int)cbYears.SelectedItem.Value;

    for (byte i = 1; i <= newValues.Count; i++)
    {
        if ((decimal?)newValues[i] != (decimal?)oldValues[i])
        {
            if (newValues[i] == null)
            {
                // The value was removed
                // so remove the record from table
                DataService.RemoveImport(creditCardID, year, i);
                continue;
            }

            if (oldValues[i] == null)
            {
                // The value was inexistant
                // add record to the table
                DataService.AddImport(creditCardID, year, i, (decimal)newValues[i]);
                continue;
            }

            // it's a change
            // update the record in the table
            DataService.UpdateImport(creditCardID, year, i, (decimal)newValues[i]);
        }
    }

    e.Cancel = true;
}

What happens in this event is that a request for updating rows is sent. We will receive the old values that (of interested updating columns) and the new one that were chosen by the user, in a form of OrderedDictionaries, exposed by two properties in our event argument. The properties are called logically NewValues and OldValues. As OrderedDictionaries are not so handy for processing, I will create two Hashtables and copy the data for both properties from OrderedDictionaries to Hashtables. Meanwhile I’m copying, I will cast eventual values to a nullable decimals and if the value equals zero, I will just insert null instead of the value. Next is retrieving other necessary data as kredit card ID (stored as editing row key and exposed to as always via the event argument) and the current year, which we will get out of our check box.
Now we will process all of our columns values one by one. If the specific column value in Hashtable that contains old values do not match to the one in the new values Hashtable, it means that the value for that column is changed and we need to determine what kind of change occurred before we decide which action to take. If the new value is null it means that we need to remove this item from database therefor we will invoke the appropriate method that we created earlier and pass all the parameters as requested. If the old value is null, it means that we do not have an entry for this item in our database, therefor we will add a new item via a dedicated method. At last if the value exists in both Hashtables we will need to update the value to the new one.
At the end we will set the Cancel property of our event argument to false. The e.Cancel = true allows us to prevent the ASPxGridView from trying to update data by the control itself. There is one small detail left, after we persist the data, we should indicate to the grid that editing is over so it can get back in the non edit mode. The best thing to achieve this is to call CancelEdit method on server side each time we bind the grid (even id sometimes it is not necessary it is not a big overhead and makes sure that each time we update data we make sure that the grid is not in edit mode). Consequently our gvImports_DataBinding event will change in the following way:

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);
    gvImports.CancelEdit();
}

Congratulations, you can now edit this grid values! Smooth, isn’t it? :)

Managing Credit Cards

As the next requirement we will add the possibility to add new credit cards, update the name of the existing ones or remove them. As for the notes we will add a new pop-up windows which will help us to perform this operations. Defining the popup window is straight forward.

<dx:ASPxPopupControl ID="popupAddCreditCard" runat="server" AllowDragging="True" ClientInstanceName="popupAddCreditCard"
    HeaderText="Add new Credit Card" Modal="True" PopupHorizontalAlign="Center"
    PopupVerticalAlign="Middle" Width="300px" CloseAction="CloseButton">
    <ContentCollection>
        <dx:PopupControlContentControl ID="PopupControlContentControl4" runat="server" SupportsDisabledAttribute="True">
            <table border="0" style="width:100%">
                <tr>
                    <td>
                        <dx:ASPxLabel ID="lbltxtAddCreditCardName" runat="server" Text="Credit Card Name">
                        </dx:ASPxLabel>
                        <dx:ASPxTextBox ID="txtAddCreditCard" ClientInstanceName="txtAddCreditCard" runat="server" Width="205px">
                            <ClientSideEvents KeyDown="txtAddCreditCard_KeyDown" />
                            <ValidationSettings Display="Dynamic" ValidationGroup="AddCreditCard" ErrorTextPosition="Bottom">
                                <RequiredField ErrorText="Name is a required field!" IsRequired="True" />
                            </ValidationSettings>
                        </dx:ASPxTextBox>
                    </td>
                </tr>
            </table>
            <hr />
            <table border="0">
                <tr>
                    <td>
                        <dx:ASPxButton ID="btnAddNewCreditCard" runat="server" Text="Add" Width="100px" AutoPostBack="False"
                            UseSubmitBehavior="False" ValidationGroup="AddCreditCard">
                            <ClientSideEvents Click="btnAddNewCreditCard_Click" />
                        </dx:ASPxButton>
                    </td>
                    <td>
                        <dx:ASPxButton ID="btnCancelNewCreditCard" runat="server" Text="Cancel" Width="100px"
                            AutoPostBack="False" UseSubmitBehavior="False" CausesValidation="False">
                            <ClientSideEvents Click="btnCancelNewCreditCard_Click" />
                        </dx:ASPxButton>
                    </td>
                </tr>
            </table>
        </dx:PopupControlContentControl>
    </ContentCollection>
</dx:ASPxPopupControl>

As for the add note popup window, we defined the similar structure for add credit card popup. The main difference is in the validation of the credit card name. We defined the field as mandatory and client side validation will be performed for that field. If empty an error message will appear. Now I will show you the code for all of the three new scripts that we attached to our client side events.

function PersistCreditCard() {
    var isValid = ASPxClientEdit.ValidateEditorsInContainer(null, 'AddCreditCard');

    if (isValid) {
        var command = 'AddNewCreditCard';

        if (currentIsCreditCardEdit)
            command = 'EditNewCreditCard';

        popupAddCreditCard.Hide();
        gvImports.PerformCallback(command + '|' + currentVisibleIndex);
    }
}

function txtAddCreditCard_KeyDown(s, e) {
    switch (e.htmlEvent.keyCode) {
        case 13:
            ASPxClientUtils.PreventEventAndBubble(e.htmlEvent);
            PersistCreditCard();
            break;
        case 27:
            popupAddCreditCard.Hide(); txtAddCreditCard.SetText('');
            break;
    }
}

function btnAddNewCreditCard_Click(s, e) { PersistCreditCard(); }
function btnCancelNewCreditCard_Click(s, e) { popupAddCreditCard.Hide(); txtAddCreditCard.SetText(''); }

The technique is the same used in the previous example, on editing and adding the imports. In case that the client side validation is performed and user have imputed the valid entries, we will preform a callback on the grid (so it can be also automatically refreshed). In the callback we will specify if the operation we are preforming is an edit or an add and in case of the edit pass the selected row index. What we are missing right now is the server side code for persisting the newly added values. To achieve that we are going to modify previously define gvImports_CustomCallback event (it is defined in the previous blog post that you can find here. It will look like this:

protected void gvImports_CustomCallback(object sender, 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;
        case "AddCreditCard":
            DataService.AddNewCreditCard(txtAddCreditCard.Text);
            break;
        case "EditCreditCard":
            DataService.UpdateCreditCard(creditCardID, txtAddCreditCard.Text);
            break;
        case "DeleteCreditCard":
            DataService.DeleteCreditCard(creditCardID);
            break;
    }

    gvImports.DataBind();
}

Based on the command we received, we will analyse other values passed as parameters and call the service method that is associated with the current action. At the end we will rebind the grid so the newly added values can be visible. Simple and straight forward, isn’t it?
I will again mention the same trick I used for the editing, and it is here for the same reason I explained earlier when I was explaining the editing of the note, but this time is made on the Add/Edit Credit Card popup. On edit we will retrieve the selected credit card name and we will populate the text box with that obtained value.

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

    int visibleIndex = int.Parse(values[0]);
    int creditCardID = (int)gvImports.GetRowValues(visibleIndex, "Id");

    txtAddCreditCard.Text = DataService.GetCreditCard(creditCardID);
    txtAddCreditCard.Focus();
}

Marking the values in the grid

Meanwhile I was writing the code for the previous requirements I got an idea. Why not let give to user the possibility to put in evidence the values that are lower than the certain amount? Well it is a simple task and it will maybe give you the inspiration to expand this example in other ways. Let’s see how to achieve something similar.
First of all we need to create the necessary interface and in order to accomplish that we will define the following controls in our aspx file. After the definition of the GridView add the following:

<table style="width: 100%;">
    <tr>
        <td style="width: 100%;"></td>
        <td>
            <dx:ASPxLabel ID="lblMark" runat="server" Text="Mark values lower or equal to:" Width="180px">
            </dx:ASPxLabel>
        </td>
        <td>
            <dx:ASPxTextBox ID="txtMarkValue" ClientInstanceName="txtMarkValue" runat="server" Width="100px" HorizontalAlign="Right">
                <MaskSettings Mask="$ &lt;0..99999g&gt;.&lt;00..99&gt;" IncludeLiterals="DecimalSymbol" />
                <ValidationSettings Display="Dynamic">
                </ValidationSettings>
            </dx:ASPxTextBox>

        </td>
        <td>
            <dx:ASPxColorEdit ID="ceMark" ClientInstanceName="ceMark" runat="server" AllowUserInput="False" Width="100px" Color="#C0C0C0"></dx:ASPxColorEdit>
        </td>
        <td>
            <dx:ASPxButton ID="btnMark" runat="server" Text="Mark" AutoPostBack="False"
                UseSubmitBehavior="False" CausesValidation="False">
                <ClientSideEvents Click="btnMark_Click" />
            </dx:ASPxButton>
        </td>
        <td>
            <dx:ASPxButton ID="btnClear" runat="server" Text="Clear" AutoPostBack="False"
                UseSubmitBehavior="False" CausesValidation="False">
                <ClientSideEvents Click="btnClear_Click" />
            </dx:ASPxButton>
        </td>
    </tr>
</table>

As you can see, apart for the table I used to achieve a certain layout, I added a text box in which the user will specify the value that we will used as the upper limit for our cells that will be placed in evidence. Also I used a ASPxColorEdit so the user can choose the color he prefers for marking the cells. A clear and confirm button are also there. Then I associated the following code to the relevant client side events (only button clicks in our case):

function btnMark_Click(s, e) {
    gvImports.PerformCallback('Mark');
}

function btnClear_Click(s, e) {
    txtMarkValue.SetText('');
    ceMark.SetColor('#C0C0C0');
    gvImports.PerformCallback('Mark');
}

In case of clear click, I will reset the values of the color chooser and the value text box to their default, then perform a callback so if any value was previously set, it can be restored to the default value. In case of click on mark button, I will perform a callback on the grid and specify the ‘Mark’ command. Once the callback is performed and the gvImports_CustomCallback event is triggered, you will see that I do not treat the Mark command. This is because it is not necessary at this point. The only important thing is to rebind the grid. Instead in the gvImports_HtmlDataCellPrepared I will add the following at the end of the method:

protected void gvImports_HtmlDataCellPrepared(object sender, ASPxGridViewTableDataCellEventArgs e)
{
    ...

    decimal markValue = 0;
    decimal.TryParse(txtMarkValue.Text, out markValue);

    if (markValue != 0 && (decimal)e.CellValue <= markValue)
    {
        e.Cell.BackColor = ceMark.Color;
    }
}

Whit this I will check if a mark value is set and if it is a valid number I will then check the cell I’m processing in this moment. In case the cell value is less or equal to the value specified in our text box, I will change the background color of that cell and set it to the color that is indicated in our color chooser.
This shows you how can you approach the grid to add extra functionality with ease.

Exporting the data

It will all be nice but useless in case we could not retrieve our data and persist it locally or/and edit it for a presentation or maybe import it in our accounting software. Together wit the Grid, DevExpress offers us an additional control that will handle the exporting procedures. It is easy to setup and the results are great! Let’s check how to do it.
First of all we need to add this control in our page. Drag and drop from the toolbox the ASPxGridViewExporter control. Then edit your aspx code and set it to mach the following:

<dx:ASPxGridViewExporter ID="gridExport" GridViewID="gvImports"
    PaperKind="A4" Landscape="True" runat="server">
</dx:ASPxGridViewExporter>

You will already notice some properties that we specified. In order to indicate the grid from which we would like to export the data we need to specify GridViewID property and as it name suggest, set id to the grid view ID. Also we will specify the PaperKind and Landscape property to get the correct formatting of our export. Now we need some button that will request the export in different formats. There are several formats that ASPxGridViewExporter can manage by default. Check the following link for further details Exporting Data – Exporting to PDF, XLS, XLSX and RTF. We will add them to the same line in which the year selector is collocated. The final code will look like this:

<table border="0" style="width: 100%;">
    <tr style="vertical-align: middle;">
        <td style="padding-right: 5px;">
            <dx:ASPxLabel ID="lblSelectedYear" runat="server" Text="Selected Year:" Width="85px" 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" HorizontalAlign="Center">
                <ClientSideEvents SelectedIndexChanged="cbYears_SelectedIndexChanged" />
                <ItemStyle HorizontalAlign="Center" />
            </dx:ASPxComboBox>
        </td>
        <td style="width: 100%;">
            <table style="width: 100%;">
                <tr>
                    <td style="width: 100%;"></td>
                    <td style="padding-left: 170px;">
                        <dx:ASPxButton ID="btnExportExcel" runat="server" UseSubmitBehavior="False" CausesValidation="False"
                            ImageSpacing="0px" EnableTheming="False" EnableDefaultAppearance="False" ToolTip="Export to Excel"
                            OnClick="btnExportExcel_Click" Cursor="pointer">
                            <Image Url="~/images/excel.png">
                            </Image>
                        </dx:ASPxButton>
                    </td>
                    <td>
                        <dx:ASPxButton ID="btnExportPdf" runat="server" UseSubmitBehavior="False" CausesValidation="False"
                            ImageSpacing="0px" EnableTheming="False" EnableDefaultAppearance="False" ToolTip="Export to Pdf"
                            OnClick="btnExportPdf_Click" Cursor="pointer">
                            <Image Url="~/images/pdf.png">
                            </Image>
                        </dx:ASPxButton>
                    </td>
                    <td>
                        <dx:ASPxButton ID="btnExportCsv" runat="server" UseSubmitBehavior="False" CausesValidation="False"
                            ImageSpacing="0px" EnableTheming="False" EnableDefaultAppearance="False" ToolTip="Export to Csv"
                            OnClick="btnExportCsv_Click" Cursor="pointer">
                            <Image Url="~/images/csv.png">
                            </Image>
                        </dx:ASPxButton>
                    </td>
                </tr>
            </table>
        </td>
    </tr>
</table>

We added three buttons and set some of the default and well known properties as image and text. What you should notice here is the UseSubmitBehavior property set to false. In this way the button will not cause the browser’s submit mechanism. However a post-back will still be performed. So for each button we will define the event handler on the server side and it will look like this:

#region Export
protected void btnExportExcel_Click(object sender, EventArgs e)
{
    gridExport.FileName = string.Format("{0} {1}", "Credit card imports for year: ", cbYears.SelectedItem.Value);
    gridExport.WriteXlsToResponse();
}

protected void btnExportPdf_Click(object sender, EventArgs e)
{
    gridExport.FileName = string.Format("{0} {1}", "Credit card imports for year: ", cbYears.SelectedItem.Value);
    gridExport.WritePdfToResponse();
}

protected void btnExportCsv_Click(object sender, EventArgs e)
{
    gridExport.FileName = string.Format("{0} {1}", "Credit card imports for year: ", cbYears.SelectedItem.Value);
    gridExport.WriteCsvToResponse();
}
#endregion

On click of each of the defined buttons we will set the file name on a more meaningful string and we will call the respective method on the ASPxGridViewExporter. That is! Check the result!

excel2

The final result is pleasing. We added some great functionality with a little effort thanks to the possibilities offered by DevExpress controls.

Downloads and the source code

You can find the source code of my project for download here.
You can find a trial version of the requested controls here.

Final word

If I was not clear or I gave for granted some of the explanations feel free to comment and I will do my best in order to bring a proper explanation. I didn’t went in detail on all of the code as most of it was covered in the previous post, ASPxGridView Excel style – Adding notes to grid cells. If there will be interest I will further expand this example and introduce you to other nice features that we can easily lay down with DevExpress controls. Stay tuned and happy programming!

ASPxGridView Excel style – Adding notes to grid cells

Introduction

I am continuing with a series of posts about Devexpress controls. You can read the introduction about what are the Devexpress controls, how and where to get them, versioning information and prerequisites in my previous blog post that you can find at this address http://blog.majcica.com/2012/08/05/aspxgridview-master-detail-data-presentation-with-context-menus/.
What we are going to achieve in this post is a simple application that will list the credit cards and the amount spent per card per month. We will be able to add in an easy way a new credit card and edit all of the months imports. More, we will be able to add a note on each month and visually see the months containing notes.
In this example I will use a database and create two simple tables that will contain all or data. Again, this will be a simple example and it is not meant to be functionally correct. My only goal is to show you how to interact with this controls in order to achieve a certain functionality, which in this case is showing, adding and modifying notes in a similar way Excel does it.

Table of contents

  1. The necessary and the result
  2. Creating database schema
  3. Writing the data service
  4. Creating the page
  5. Defining the ASPxGridView
  6. Showing cells with notes
  7. Showing notes in a popup screen
  8. Adding, editing and removing notes
  9. Changing the year
  10. Downloads and the source code
  11. Final word

The necessary and the result

The attached project is written in Visual Studio 2012. The express version should be enough. Also you will need a Microsoft SQL Server 2012, Express or and more advanced version will be just fine. The database is part of the project and it will be automatically mounted by VS. You can change this easily be creating your own database and changing the connection string in the web config. Last but not least you will need the 12.1.7 version of DevExpress ASP.NET controls installed on your machine. If you do have a more recent version, upgrading the project should be easy. Check this post about how to get the DevExpress controls and on how to eventually upgrade the project http://blog.majcica.com/2012/08/05/aspxgridview-master-detail-data-presentation-with-context-menus/.

At the end the result should be similar to this, however there is much more, so check this post till the end.
Final Example

You can check it also LIVE. Live version persist the data in the session so each new session will reset all the data you have inserted. Also because of this, it is a bit simplified. If you are interested in the code, I can provide it to you, just ask in comments.

Creating database schema

First of all we will create a table that will store all of our credit cards. We will call it tbl_CreditCards. It will contain the following columns:

tbl_CreditCards

Another table that we will add is tbl_Imports in which all of our imports, per card, with note, will be persisted. The table is structured in the following way:

tbl_Imports

We also need to remember to add the foreign key constrain which we will call FK_tbl_Imports_tbl_CreditCards. Here is the complete SQL script that will do the necessary.

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)
)

We will also add a stored procedure that will help us retrieving data from the database. One parameter will be required, in relation of which we are going to retrieve all the data and pre-format for presentation. We need to retrieve card name, imports and notes for all the months of the indicated year.

/****** 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

In order to test our stored procedure and other queries we will insert some testing data.

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

Writing the data service

Our data service will be a simple class that will expose several static methods that will help us performing all the CRUD actions. Let’s define them.

As I would like to have a combo showing all the years present in the database plus the current and subsequent year, in order that the user can filter the list by this values, I will first prepare a method that will retrieve and prepare this data.

/// <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, string> years = new SortedDictionary<int, string>();
    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;
}

Now we need to prepare the method that will call our stored procedure and return the data:

/// <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;
}

For this first part we also need to prepare the methods that will manage our notes.

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();
        }
    }
}

Now we are ready to start writing our page.

Creating the page

In our site default page we will add an ASPxLabel and ASPxComboBox. First will show the text Selected Year and the second one will list all the years that we will retrieve via GetYears method. Please forgive the usage of the tables for setting up the layout, div’s always bothered me, specially when vertical alignment is in question. Also I will add an ObjectDataSource that will be used for binding.

<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"></asp:ObjectDataSource>

As you can see, I used the method GetYears as my select method and I set TextField and ValueField to correct names in order to bind to a Dictionary object. For the ASPxComboBox I specified a client side event in which later on I will create a callback to reflect the changes. Right now the code will not run correctly because we have no client side event in our JavaScript file specified, but do not worry, as soon as we specify our grid, we will write down the necessary. Last thing is the definition of server side DataBound event, in which we will make the current year as selected in our ASPxComboBox. This is the missing code.

protected void cbYears_DataBound(object sender, EventArgs e)
{
    // Select current year as default year to show
    cbYears.SelectedIndex = cbYears.Items.IndexOfValue(DateTime.Now.Year);
}

Now if you run your solution you should see something similar:

Populated Combo

Let’s now define the grid.

Defining the ASPxGridView

The longest and the most laborious thing is defining the columns. I will show you the complete code of the grid and then discuss all the properties and events I have used.

    <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>

As you can see, the longest and most tedious part is defining the columns. We defined a column for a card name, one for each month and a column that will show the total per card. Total column is unbound and I we will perform the calculations for this column in CustomUnboundColumnData event. As you can imagine FieldName property is the name of a column in the binded DataTable, Caption is a column header text and ToolTip is well a tool tip for that column header. DisplayFormatString is a formatting string option (N2 is this case formats the value as integral and decimal digits, group separators, and a decimal separator with optional negative sign, with decimal precision of two) and NullDisplayText (when no data is supplied) is set for styling purposes to a slash character.
For the card name and total column, we also specified what should be shown when the row is in edit.
A TotalSummary feature from the ASPxGridView is used, it will show an extra row and the totals for each specified column. By adding ASPxSummaryItem we are specifying the column that needs to be took in consideration (by indicating ShowInColumn property we are positiong in the indicated column the result of our summay), a SummaryType that should be performed (Sum in this case) and a field on which the calculation should be based.
We set some properties on the grid so let’s check what is the behavior of these settings. SettingsPager mode ShowAllRecords basically disables the pager meanwhile the SettingsEditing mode Inline sets the editing mode to inline. We also decided to show a Footer (necessary for Summary row) and to show a title panel, an extra row on top of the grid that will show a specific label, at example the meaning of the data that is shown in the grid, which we later set to “Credit Cards Year Overview” in SettingsText Title property.
Also we disabled the Drag and Drop on columns (re-orderable columns feature of ASPxGridView that is not desired in this case), disable out of the box sorting feature feature and enabled a row visual tracking (EnableRowHotTrack makes the row that is under the mouse cursor highlighted).
In order to perform a custom calculation for our Total column we defined the CustomUnboundColumnData event. This is his server side implementation.

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"));
    }
}

The even argument helps us by putting on our disposition the column name that he is processing at that moment, so only if he is processing a desired column we assign that columns row value to a sum of all months columns values. If column value is DBNull.Value then zero is used, else a column value is converted to Decimal and then summed.
It is time to bind our grid and check the result.
I choose to use the DataBinding event in order to assign the right source to the grid.

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);
}

Before indicating the designed method to the grids DataSource I will check the parameter and if for any reason it is not indicated in the ComboBox I will set it to the current year. The effective bind method will be called from the Page_Load event only on first show (and not on postbacks).

protected void Page_Load(object sender, EventArgs e)
{
    if (!Page.IsPostBack)
    {
        gvImports.DataBind();
    }
}

Now we can run our code and check the result.

Grid After First Binding
Pretty neat and nice result for a relatively small effort, isn’t it?

Showing cells with notes

Now the idea is to set for each cell that contains a note, a specific background image so the user on the first sight can understand that there is a note for that import. In order to set dynamically a background on a cell we need to declare HtmlDataCellPrepared on our grid. Don’t forget to set the event in the ASPX file (OnHtmlDataCellPrepared=”gvImports_HtmlDataCellPrepared”) and set the following method in your page:

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");
    }
}

First line of code is a shortcut, which says that if the cell we are processing is in the Name or Total column (columns that can’t contain notes) or if CellValue is empty, we can stop processing it. As our DataSource contains extra columns that contain notes, the name of the column is N plus the number of the month column to which it is associated to, we can easly establish the name of the column by checking the current processing items column and request the note by e.GetValue method in which we will pass the name of the Note column we are searching the value for (at example if we are processing the January column which is called 1, we will search on the current processing row the value of the column N1 which will eventually contain the note.
Now if the note is not DBNull.Value and note is not null, we will add the processing cell style attributes background-image and background-repeat to desired values, for the first one the image we chose and for the second one the repeat behavior (which in this case should not repeat).
The result is that all cells that contains a note do have a different background and they can be quickly spotted by the user.

Showing notes in a popup screen

As till now we are only showing the cells that contains some notes, it will be nice if we add a way to show, add and remove them. Let’s first start with showing them. Showing the notes will be performed by passing the mouse over the cell that contains the note and after a given amount of time a div with the note text will be shown. We are going to achieve this in the following way. At first we will define a window that will show the note itself and we will use ASPxPopupControl to achieve this:

<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>

Well most of the properties are self explanatory, I just added an ASPxPopupControl and as his content I added a simple ASPxLabel control. Notice that I assigned a specific client instance name for the ASPxPopupControl control in order to call the methods and properties on client side with ease.
Now we need to define JavaScript functions that will be assigned to each cell that contains a note on the onmouseover and onmouseout client side events.

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);
}

This code maybe seems a bit complicated and tricky, so I will try to explain it. When a mouse gets over the interested cell, the ShowPopup method is executed. We will set the text of the lablel control that we just add in our ASPxPopupControl and close any eventually opened pop-up windows (just to be sure). Then a flag reservPopup is set to true (this is used for controlling the time out, in the case that a mouse cell gets out of the cell, this flag will be set to false, it means that if the showing a popup was requested and the timeout is not yet expired, the popup will not be show, basically if you were quicker than the given timeout triggers the Show function on window) and a function is scheduled for the execution in 1000 milliseconds. This function will check if it should show the pop-up with the note and if so it will call the method ShowAtElementByID of our ASPxPopupControl.
What we are missing now is associating this function to our cells and in order to achieve that, we need to modify our HtmlDataCellPrepared event in the following way:

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", "<br />");

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

As you can see, we added three new lines. In the first one we are preparing our note to be “Html compatible” by encoding any special char to a proper html notation and substituting the new line character with his HTML equivalent (<br /> break row in this case). Also as the ShowPopup functions second parameter we will pass the cells client ID so the pop-up can be shown next to the cell. Now we can run and test our code.

Well nice result!

Adding, editing and removing notes

As another requirement it will be nice to show a popup menu on the right click of any cell and propose to Add, Edit or Remove the note from that cell. It is interesting and we will jump on work straight forward by defining an ASPxPopupMenu with the necessary items.

<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>

Inside our ASPxPopupMenu we defined three menu items, one per each specific information we are going to perform. We also assigned a menus ClientInstanceName and a client side event ItemClick implementation of which we will see later in text.
What we now need to prepare is the interface that will be used for Adding and editing notes. We are going to define another ASPxPopupControl controls and define it’s content.

<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>

Popup is set in a similar way as a previous one, the content only contains different controls. You will see that I used a ASPxMemo control which is very similar to a multiline text box, and two buttons. Non of these buttons causes validation or generates a post back on click and both have an client side event associated to their client side click event. Let’s check first the the implementation of the ASPxPopupMenu client side events then we will check the rest.

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;
    }
}

When an item in the GridMenu is clicked, this is the code that is executed on the client side. We are checking in the switch statement the name of the clicked item and based on that executing the indicated operations. In case of AddNote we are setting a flag currentIsNoteEdit to false. This flag will be used to understand if we are editing a note or we are adding a new onse (since we will use the same popup for both operations). Then we make sure that the text of our ASPxMemo control is set to empty and set a popup control header to ‘Add Note’ (same reason as before, it is a shared control for between different actions). Then we are showing the popup and setting the focus on ASPxMemo control. Similar things for EditNote except that as the last thing we are performing a callback on a popup control. We do that so we can retrieve the note data as we do not have them available on client side (at least not easily available, still the will be html encoded and we will have some trouble re-encoding the same data). If you check popupAddNote definition you will see that we already declared OnWindowCallback event, now let’s check it’s implementation:

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();
}

You could deduce from the JavaScript call that we were passing as arguments to this callback two values, currentVisibleIndex and currentFieldName. First one is self explanatory meanwhile the second one is the column name, which in our case is the related month. I will get back on this second parameter later. Once I receive this data on server side, I will retrieve the necessary and set relevant properties.
Let’s get back on the last item of the ASPxPopupMenu and write down and explain the code. The idea is to show a confirmation dialog before deleting a note. I will now create a dialog that will be reusable even later, for other entities.

<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>

As earlier we defined a ASPxPopupControl and created the content that should be shown. We have two buttons, one for canceling this action and another for confirming it. Both have no server side events or actions assigned, everything is performed on client side click in for that foreseen event. This events are defined in the following way:

function btnCancelDelete_Click(s, e) { popupConfirmDelete.Hide(); }

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

    if (deleteCaller == 'Note')
        gvImports.PerformCallback('DeleteNote|' + currentVisibleIndex + '|' + currentFieldName);
}

First method is self explanatory, on cancel click call Hide method on popup control. Second method also as first thing hides the popup then for the specific caller executes a callback with in arguments the command that should be performed, selected item’s visible index and column name.
This as all other commands are performed in the grids CustomCallback event. This is the code I used:

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();
}

If no parameter is specified, ignore the callback. Split the string by the separation character that I used for convection for separating different values in a string. If the retrieved array contains more then one element, aain by my own convection it means that the second parameter indicates a visible index value as long the first value, that indicates the action to perform, isn’t ChangeYear. In this case the second value is indicating the year on which the grid should be bound. If the array contains more than two values then the third value indicates the month and here it is what really the currentFieldName that we were passing is. Now, once I have all the data, I can check the command and perform the desired action. All the calls to the database were already written down on the start, so I do not need to do more than just call a specific method od the DataService and pass just decoded parameters. Easy? Clean at least (I hope so).
Seems that everything is wired up, but we do not have yet anything that will pop up our menu on the grid. In order to achieve that we need to modify our old acquaintance, the HtmlDataCellPrepared event. We will modify it in the following way:

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", "<br />");

        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));
    }
}

As you can notice I added a new attribute on the cell, oncontextmenu, and associated to it a client side function OnCellContextMenu. This client side function that we are going to specify, requires several parameters, the event, rows visible index, column name, indication if the field contains note and as the last one if the cell contains a value. All this information will be useful when we are going to decide what to show and later on for having all the necessary data in order to execute the operations.
We are setting this attribute in three different places, for three different types of cells. The first one is for the cells that has no value and on which there should not be show the context menu. We will handle this on client side, however this is necessary for future improvements that you will see in my next blog post. Then we are assigning the same to all the cells that contains a note and all the cells that doesn’t however with different parameters. Client side function looks like this.

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); }

Well we are persisting the visible index and field name in variables that are globally accessible, so we can use them later. Now we will hide or show different menu items base on what are actually the operations that we can perform. If the cell already contains an note, we can modify the note or delete it, otherwise if the cell has a value but doesn’t the note we can only add a new note. This basically ecplains the next three lines of code as them translate that into the code. Now we will determinate the windows event as it is handled in a specific way on different browsers and then prevent this event and bubble by using a helper method that is available in DevExpress client site framework. This event determination is necessary in order to make the popup menu work properly on all major browsers of today. And the last important thing is showing the menu itself. We will show the menu only on cells that has a value and we will achieve that by calling a ShowAtPos method and calculating the right coordinates again via DevExpress framework helpers. That’s it! Let’s run our example and test it. The following screen is what you should see once you try to add a new note.

Final Add Note

Changing the year

Something we almost forgot is to set the client event code that will change the selected year. Initially we only defined the event on client side but actually with no implementation. As the server side code will already manage the “ChangeYear” parameter, we only need to perform the right callback:

function cbYears_SelectedIndexChanged(s, e) { gvImports.PerformCallback('ChangeYear|' + s.GetValue()); }

As you saw in other occasions we are making a callback on the ASPxGridView and passing a command parameter and the newly selected value.

Downloads and the source code

You can find the source code of my project for download here.
You can find a trial version of the requested controls here.

Final word

This is it for the first part, if you have any question, feel free to comment. I will use this example as a base for the following blog post in which I will extend the functionalities by introducing inline editing, CRUD operations on credit cards, export and more, so stay tuned!

ASPxGridView master-detail data presentation with context menus

Introduction

As many companies I worked for are using DevExpress controls I decided to write a couple of posts about some real life situations and ways they can be solved by using DevExpress ASP.NET Suite. In this and following post I will show you a couple of techniques on how to achieve a certain behavior that goes slight further than the demo examples that you can find on DevExpress site.
I will be using ASP.NET suite of controls, more specifically Web Forms.

At the end of the article this is the expected result:

You can also check the LIVE DEMO.

Table of contents

  1. What are DevExpress controls?
  2. Requirements
  3. The project
  4. Creating a data source
  5. The web page
  6. Defining a detail grid
  7. Adding a context menu to the detail grid
  8. Programmatically disabling menu items
  9. Aesthetic changes
  10. Downloads and the source code
  11. Notes and other resources

What are DevExpress controls?

A set of controls that enrich your toolbox by adding several controls that are not present in the standard ASP.NET controls and some of the controls that are offered as a good substitute to already existing controls. All of the DevExpress controls are rich by the properties, methods and events both on client and server side, giving you the possibility to achieve results that otherwise will require a lot more extra coding.

Requirements

All of my examples are written for .NET 4.0 with Visual Studio 2010 using the version v2012 vol 1.5 of DevExpress controls. You can find a trial version of the requested controls here DevExpress Demo. Any version of Visual Studio is just fine, from Express to Ultimate. Also you can easily migrate this project to .NET 3.5 if needed. In case that the version of DevExpress controls I used is not available anymore, it should be easy to upgrade the project by DXperience Project Converter. For more information’s on project converter check DevExpress web site.

The project

I will start with the default Visual Studio ASP.NET Web Application.
Considering this example just a practice about the UI and the controls itself, I will put no emphases on the data source and just create a super simple data model. We will have two data entities, User and Project. As a cardinality, we have a many-to-many relationship, so one user can be related to many projects as a project can have several users. Also I will create a class called DataService that will create a couple of values that will represent our data.

Let’s start!

Creating a data source

First we will create a Project class. In the default constructor we will set the project status property to new. Except for this, we will have three properties, an ID, project name, and a project status.

public class Project
{
    public Project()
    {
        Status = ProjectStatus.New;
    }

    public int ID { get; set; }
    public string Name { get; set; }
    public ProjectStatus Status { get; set; }
}

As you can see, project status is an enumerator, so let’s define it together with other possible project statuses.

public enum ProjectStatus
{
    New,
    InProgress,
    Failed,
    Done
}

Now we will define the user class. It has several properties and one public method. The method returns the number of associated projects of the current customer instance.

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;
    }
}

This is a very simple model and probably in a real life situation your model will be richer with properties and methods.
Next to come is a class that will create several instances of the model classes and return them via a method. In this way we can have easily all the data we need for our example. The code I used is following, you can add other data if in search of a particular behavior.

[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;
    }
}

Now our data source is ready. The next thing to care of is the web page itself.

The web page

Add the grid by drag dropping the ASPxGridView control in the page. Modify the properties in order to match the following:

<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>

What we did is changing the grids ID to gvMaster, indicating the Key field name by setting the KeyFieldName to “ID” and specifying the columns that will be shown together with mapping a column field name to the desired model property. Now we need to bind the grid and we will do it from the code.
In order to be able to show the changes, we will save our data in a session and in order to ease this operation we will create a property that will perform all of this check and operations for us. Get in the page’s code file and declare the following.

public List<User> Users
{
    get
    {
        if (Session["Data"] == null)
            Session["Data"] = DataService.GetUsers();

        return (List<User>)Session["Data"];
    }
    set { Session["Data"] = value; }
}

When the property is requested for the first time, the session item is null, then we will recall the method that we created previously and save the data in the session. This is not a technique that you will use in a real life application, because probably you will recall the data from the database at a certain point and eventually cache it. As explain this is not the goal of this article, I will just mention it.
Now it’s time to bind the grid to this property.

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        gvMaster.DataSource = Users;
        gvMaster.DataBind();
    }
}

If you run the code now you should see the following (or a similar screen, don’t worry if the theme that is applied doesn’t look the same, we will came to that later).

Defining a detail grid

In order to add a detail grid to a master grid, we first need to define a template for the detail row. Inside the aspx file get inside the definition of ASPxGridView and open the <Templates> section then inside the newly created section a new one called <DetailRow>. Close properly both sections. In the Detail Row template add a new ASPxGridView and define as for master grid the columns and some properties. Also do not forget to set the ShowDetailRow property to true. At the end your code should look like this.

<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>

Now we need to handle the binding of the detail grid. Before that we will check for each row in the master grid if there is the data for the detail grid and if not, hide the plus sign. To achieve that, we need to declare the OnDetailRowGetButtonVisibility event on the master grid. The code of your master grid should look like this

<dx:ASPxGridView ID="gvMaster" runat="server" AutoGenerateColumns="False" KeyFieldName="ID"
    Width="100%" OnDetailRowGetButtonVisibility="gvMaster_DetailRowGetButtonVisibility">

In the server side event code we need to check if there are detail data for each master element.

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;
}

For the less experienced, I will quickly explain this code. In order to get the row value we will use the argument that is passed to the event which contains the currently processing row visible index and with that information retrieve the value of the ID field of that row. The following code gvMaster.GetRowValues(e.VisibleIndex, "ID")) will give us the the value of ID filed for the current row. Then we will retrieve the User class instance for a given ID and check if it has project. In case that this user is not associated to any project we will hide the plus sign (button) for that row with by setting the argument property ButtonState to hidden.
Now let’s bind detail grid.
Each time the user click’s on the plus sign, a callback will be automatically generated by the grid and on the server side, binding event for the detail grid will be raised. OnBeforePerformDataSelect event in the detail grid is the right one for indicating the data source on which the current detail grid should be bind. Define the previously mentioned event:

<dx:ASPxGridView ID="gvDetail" runat="server" Width="100%" 
    OnBeforePerformDataSelect="gvDetail_BeforePerformDataSelect" KeyFieldName="ID">

And bind the data in that event.

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;
}

In order to refer to a proper object we need to cast the sender of the event to a ASPxGridView. Then DevExpress grid comes in our help with GetMasterRowKeyValue() method, which as it’s name says, will return the key value of the master grid in which our current detail grid is defined. Whit that value, which is basically the User ID, we can retrieve the necessary data which we will set as a DataSource of the current detail grid.
That’s it, your master detail grid should work now and this is how it should look like.

If your solution is not looking completely the same do not worry, important is that it compiles and shows the data correctly for now.

You can see that I’m constantly pointing for the detail grid the current fact. This is important, because we can have multiple detail grid’s in the page, so we always need to refer to a proper object. Always think about that when you are working with detail grid.

Adding a context menu to the detail grid

This is a bit more complicated task but as you will see a quite simple way to achieve this.
Start with adding a client side event ContextMenu on the detail grid:

<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>

And then defining a menu with couple one item in it (just drag and drop PopupMenu control from the toolbox in the page):

<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>

With DevExpress controls we can define a client side (JavaScript) events in the aspx page. As on the server side, the will fire behind a certain event. We need to assign a function name that we are planning to execute on client side once the event is fired. Two parameters will be passed to our function, first is the control that generated the event (sender) and the event arguments. Each event and control has it’s own arguments. DevExpress controls are very function rich on client side, and you can consult the documentation to check all available client side events, functions and properties.

In the following Reference you can check the available client side functionality for the ASPxGridView. If you are interested in the other controls, just browse the interested control namespace that ends with Script.

In the page header (or in a separate .js file) define the following function:

<script type="text/javascript">
    function OnContextMenu(s, e) {
        if (e.objectType == 'row') {
            detailContextMenu.ShowAtPos(ASPxClientUtils.GetEventX(e.htmlEvent), ASPxClientUtils.GetEventY(e.htmlEvent));
        }
    }
</script>

In this function we will check if the context menu event is raised on a grid header or on the grid row. If it is a grid row we should popup a context menu. We can refer to the control on the client side by the ClientInstanceName that we defined for that control in the aspx file, this is done in this example. Each time you put a DevExpress control in your page, you will automatically have at your disposition an utility class called ASPxClientUtils containing several methods that can help you reducing your js code.

Now each time you right click the detail grid row, a context menu that you defined will be shown. What we are missing is the action that needs to be performed once the user chooses a context menu. In order to achieve this we need to add a client side event to our 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>

There is some more work to do. As we are going to use a callback method of the detail grid to process the action on the server side, we need to find out the right detail grid that needs to be updated. In order to achieve so, we need to modify our previously defined method 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>

What we did here is to save a reference to a detail grid and current visible index (row index on which the mouse was positioned when user right clicked) so we can reused it the Popup ItemClick event that we are going to define:

<script type="text/javascript">
    function detailContextMenu_ItemClick(s, e) {
        if (e.item.name == 'cmdResetTest') {
            currentDetailGrid.PerformCallback(currentVisibleIndex);
        }
    }
</script>

Once the menu item is chosen we will check if the item is the right one (this is useful if we have several items and we need to perform different actions based on chosen item) then request a callback for the grid on which user is operating right now. We will pass the current visible index as a parameter so we can spot the right element on which we are trying to apply our action. Before we can declare this server side event, we need to specify it in the aspx file:

<dx:ASPxGridView ID="gvDetail" runat="server" Width="100%" OnBeforePerformDataSelect="gvDetail_BeforePerformDataSelect"
    OnCustomCallback="gvDetail_CustomCallback" KeyFieldName="ID">

And than manage this event on server side:

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();
}

As before, for simplicity we will cast the sender argument to ASPxGridView variable called grid. Then we will retrieve the ID of the project that was selected. The argument we passed before on client side to the PerformCallback function will come handy right now as it will store the necessary data in order to find the interested project (by parsing the e.Parameters property). Next value we need to get is the user ID for which this detail grid is showing the associated projects. We can get it by a handy server side method GetMasterRowKeyValue() which will return a key value of the master grid (as you rememer we defined as a KeyFieldName the ID property of interested entities). Now, once we have the necessary data we can perform the desired actions and rebind the detail grid.

You can now add different actions in the Popup menu and manage them by passing a qualifier in the argument, parsing the argument and performing different operations. You will see this technique in my next blog post, stay tuned.

Programmatically disabling menu items

Unfortunately in this example the user can choose to reset the status of projects that are not in an invalid state. In order to disable the menu item if the state is not “resetable” we will need to make some changes in our code non less passing more information to client side.
Before modifying the JavaScript we will make some considerations. In order to disable an item in the menu, we need to know the condition on which to do it. We can say that the Reset item needs to be disable when the project status is New. This status information we need to pass to the client side somehow. One way to achieve this is to store the status information together with the key value in a custom property. All the DeExpress controls have the possibility to easily add information from server side that will be brought and exposed on client side. This feature is called Custom Properties and you can find more information’s about them here. My technique is to save the Dictionary element to a custom property which will be seen as an array from JavaScript. All what I’m saying may sound confusing, so let’s see an real example.

First of all we will subscribe to OnHtmlRowCreated event. Modify the detail grid in the following way:

<dx:ASPxGridView ID="gvDetail" runat="server" Width="100%" OnBeforePerformDataSelect="gvDetail_BeforePerformDataSelect"
    OnCustomCallback="gvDetail_CustomCallback" OnHtmlRowCreated="gvDetail_HtmlRowCreated" KeyFieldName="ID">

Then write down the following code:

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);
    }
}

This code can seem complex but it isn’t. For each row that is going to be rendered I’m getting it’s visible index value, checking if I already have that value in my Dictionary type variable. If not, I’m adding a new item to my Dictionary together with it’s status. If changed, I’m persisting the new value to a custom JS property of the grid.
This now means that I can easily retrieve this information’s on client side. Modify your OnContextMenu function in the following way:

<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>

We first need to retrieve the menu item then set the enable property based on custom JS property value.

That’s all, try your code, it should work.

Aesthetic changes

In order to make your solution look like mine, you will also need to set the theme and a couple of other properties that I will explain here.
First of all the theme. DevExpress control ships with a several themes, check the following web page for more details on how to deploy a theme to your solution. I applied the Acqua theme by importing the necessary files to my solution and changing the web.config in the following way (if not present add the following code inside the system.web section):

<pages theme="Aqua">
</pages>

I also added to both grids the title panel and the title itself:

<Settings ShowTitlePanel="True" />
<SettingsText Title="Master Grid / Detail for detail grid" />

In order to make a clicked row visually different I also enabled the AllowFocusedRow property. As it will set focus only on left mouse click, I also modified my JS OnContextMenu function, so it will get focused also on the right mouse click:

<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>

The EnableRowHotTrack was also enabled so the grid displays the hot tracked row (a row located under the mouse pointer). You can read more about all these properties on DevExpress site.

Downloads and the source code

You can find the source code of my project for download here.
You can find a trial version of the requested controls here.

Notes and other resources

If a specific version of the controls is not available, you can upgrade this project to the latest version of controls. Use DevXperience Project Converter Tool for upgrading the project. Read more on how to use this tool in the following blog post.
You can find other examples on Master-Detail functionality on DevExpress site. This is Master-Detail – Detail Grid example, and the following is the usage of a tab control inside the detail row template.
In the DevExpress support site you will find several examples with the source code on different techniques this link will show you the solutions for a specific master-detail challenges.

Till the next post!

Cheers!

A good practice for mapping TFS collections on local path

How many times did you asked yourself, what now, in front of a very simple question? Then you chose a first thing that came under your mouse? I did it many times! Later this lead me also many times to a problem. One of this banal situations is mapping a local path for a TFS Collections/Projects. Hands up who went creating a directory on C drive and indicating a newly created map as TFS local path! Great, I did the same! However later on, based on experience, I found a way of mapping collections which I believe is a good practice. In the following lines I will share my ideas with you. Any suggestion or observation is welcome, so feel free to comment this post.

Why is not a good idea to just create a folder Projects on a C drive and map everything under it? Well, at first this PC may be used by others, this arise a problem, that can be first of security nature, second can create confusion and problems if the new user chooses the same directory for mapping.

Based on this information, a good place where to store your code is user folder. Generally you can find your user folder in C:\Users\%username% on Vista, 7 and 8, C:\Documents and settings\%username% on XP and 2000. What I follow as a rule is to create a top folder called Projects and then inside that folder a sub folder for every collection I have access to. Supposing that my collection is called ACNProject the full path on my PC looks like C:\Users\mario.FLORES\Projects\ACNProject.

However in some cases this can be a problem. If you are using a roaming profile, you will need to do a couple of tweaks. You must exclude this newly create folder from synchronization and in order to be sure about that, you need to check the following registry key: HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\ExcludeProfileDirs. You will then find a similar content as on the following image:

Registry editor ExcludeProileDirs key

Now you need to edit this key by adding at the end the folder name that is situated in Users directory, in this case Projects. Different folders are separated by ; sign. If this became a company rule, you can avoid this step by applying the same changes via a group policy.
In case you do not have the rights for editing the registry and you are still using a roaming profile, a good place where to store the files can be C:\Documents and settings\%username% \AppData\Local which by default is never synced.

Now we are ready to map our collection. Open Visual Studio and Team Explorer.

Team Explorer mapping the collection in Visual Studio

Click on local path and digit the above mentioned path. Make sure that you clicked on collection folder (the $ sign will be visualized as a server folder) and that Recursive check is on. In this way for all the projects in that collection, automatically a folder will be created and mapping will have a recursive effect (all the projects in the collection will be automatically mapped).

Mapping the collection in TFS

Now you will be prompted about getting the latest version of all data from the server, choose no if there are many projects and you need a specific one and then get the latest version of that specific project, otherwise choose yes.

Certainly there can be a case that this approach will not fit, however I consider this a good practice. If you have any suggestions or comments, they are always welcome and I’m always ready to reconsider this task if anything meaningful pops up.

Cheers!

Natural sort order comparison – Part 1

Introduction

One of the things that are at the origin of my passion for programming is the surprise factor. No matter how many experience and studies you make any new assignment brings something new, something not completely explored.
My excursions to the UI are not so frequent and because of that I do not encounter so often some of classical problems that usually are related to UI, in particular sorting cases.
Recently I worked on a project that in UI lists different versions of one well defined entity. The main property of the entity is the version number and it is type of string. Mentioned property, along with others, is shown inside a ListView control. Sorting the list using the standard string comparison was not an option. By using a standard string comparison, numerical part of the string is not treated as a number, instead is treated as a string, which gives that the string “v1.9” comes after “v1.10”. Actually, if we consider a dot as a decimal separator, this is a correct result. However, in our case we need to consider a dot as a major-minor separator and as this we need to treat the minor part as an integer. In order to make the example easier, we will use only major number as version indicator. The entity we are going to list will contain 13 different versions, from “v1” to “v13”. Using a standard string comparison, once we order our list on version field, the result will be the following:

v1
v10
v11
v12
v13
v2

v9

Actually this is correct result if we consider the numbers as ASCII characters, but if we, as usually humans do, would like to consider the numeric part as an integer, this is an undesirable result. What we are expecting to have is the following:

v1
v2

v9
v10
v11
v12
v13

You will find a similar behavior in your Windows Explorer each time you order the files by name.

Now how do we solve this task?

Getting started

In the front of a similar problem, I don’t like just to Google a couple of keywords and use the first solution that appears on the search list. Also, the application is flexible enough so we can easily encounter different user settings and the sorting requirements may change. In order to see a bigger picture I decided to explore this area a bit more in detail.
At the end I found two viable approaches to this problem

  • Using a StrCmpLogicalW function via P/Invoke
  • Writing a custom class that implements IComparer interface and a custom comparison algorithm
    Both methods are valid, each one with its own pros and cons. So let’s see them both in detail.

StrCmpLogicalW via P/Invoke

Inside the shlwapi.dll you will find a method called StrCmpLogicalW, which, as said in method’s description, “Compares two Unicode strings. Digits in the strings are considered as numerical content rather than text.”. This is a pretty handy method, it is easy to implement and it is fast. However it has couple of cons that can make you choose different approach, as example it shows different behavior on Windows XP instead of Windows Vista. Also it is not implemented in Windows 2000 and numerical content is always treated as integer. However, it is very unlikely that you will find yourself in any of listed situations and that the standard functionalities offered by StrCmpLogicalW will be a limitation.

The implementation

For the less experienced programmers, I will show a decent way to create a class that implements IComparer interface and uses the StrCmpLogicalW class in order to calculate the comparison result.
As first we should declare a helper class that will make the calls to the StrCmpLogicalW easier.

[SuppressUnmanagedCodeSecurity]
internal static class SafeNativeMethods
{
    [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
    public static extern int StrCmpLogicalW(string psz1, string psz2);
}

Actually there is not a lot to say about this class. It is an internal (the class can be accessed by any code in the same assembly, but not from another assembly) and is declared as static (indicating that it contains only static members). SuppressUnmanagedCodeSecurity attribute is primarily used to increase performance, for further details consult the following page http://msdn.microsoft.com/en-us/library/ms182311(v=vs.80).aspx.
Now we have all the necessary in order to implement the following class

public sealed class NaturalOrderComparerNative : IComparer, IComparer<string>
{
    public int Compare(string a, string b)
    {
        return SafeNativeMethods.StrCmpLogicalW(a, b);
    }
 
    public int Compare(object x, object y)
    {
        string a = x as string;
        string b = y as string;
 
        return Compare(a, b);
    }
}

Implementing the IComparer interface is straight forward as it requires only Compare method to be coded. Compare method has the following signature public int Compare(object x, object y), it compares given objects and returns a value indicating whether first is less than, equal to, or greater than the second one. In case that x is less than y it will return -1, if x equals y will return 0 (zero) and in case x is greater than y will return 1.
I also implemented a generic version of this interface, IComparer<string>. I think that there are no more explanations to do about this small piece of code. If not, just comment under the article and I will try to do my best in order to make this clear.
So let’s summarize:

PROS CONS
Performances Platform dependent
Simple implementation Non customizable
Proven algorithm

In the second part of this article (blog post) we will see how to implement a custom algorithm and some usage examples, together with full source code in C# and VB.

Follow us so you can be notified about the new posts.

Certificates to DB and Back – Part 1

Foreword

I wrote this article at the beginning of 2011. As I was developing a WCF client application which was using Message Security with Mutual Certificates Exchange and because of a particular application request, I developed this part of code in order to facilitate maintenance and deployment of the interested application.

Now, if you are trying just simply to import/export the complete certificate and not recreate the certificate base on the his signature and the private key, framework will do already everything you need.

// Create new cert from the file and export it to a byte array
X509Certificate2 cert = new X509Certificate2(@"Cert.pfx", "password",
    X509KeyStorageFlags.Exportable);
byte[] originalExported = cert.Export(X509ContentType.Pfx, "password");

// Create new cert and import it from the byte array
X509Certificate2 clonedCert = new X509Certificate2();
clonedCert.Import(originalExported, "password", X509KeyStorageFlags.Exportable);

// export the cloned certificate with a differenet password
byte[] exported = clonedCert.Export(X509ContentType.Pfx, "newpassword");

// save the new certificate to a file
using (Stream stream = File.Create(@"E:\ClonedCert.pfx"))
{
    stream.Write(exported, 0, exported.Length);
}

So if you do not need to combine your private.key and public.cer on the runtime you do not need to read further, otherwise continue and you may find the solution to your challenge.

You can download the source code here.

Table of contents

Certificates to DB and Back – Part 1

  1. Introduction
  2. Getting started
  3. Creating Certificates

Certificates to DB and Back – Part 2

  1. Loading the Certificate
  2. Decoding RSA Private Key
  3. Creating the X509Certificate2
  4. Demo Application
  5. Further Improvements
  6. License

Introduction

In today’s applications, the use of web services is constantly growing. In the web services panorama, there are different ways of managing the authentications. One of the common methods is mutual certificates exchange.

Imagine that your application uses a web service that needs a customer’s certificate in order to correctly authenticate with the endpoint. Now, you can explain to your customer how to create the certificate, by generating one, the RSA key, and then creating a pfx container, positioning pfxcontainer file in the suggested folder, then inserting in your application the correct path to the pfx file. Hmm, it is a lot of work and is quite complicated. And what if you need to deploy it on many PCs for the same customer and your service is accessed directly from the client? If you exclude tricks, there is a lot of extra work.

What if you store only the pem strings of your certificate and private key, together with the password in database? Nice, but creating a valid X509Certificate object that contains the private key on .NET is not a trivial task. Don’t worry, this guide can help you.

In case some of the terms used in this introduction are not clear or known to you, but you still want to follow this guide, you will find some answers that will help you at the links given below:

Getting Started

In order to start and try the solution proposed in this guide, you need Microsoft Visual Studio 2010 in any of his many versions. This code will work even on previous versions of Visual Studio editions as 2008 or even 2005, but you’ll need to set up the project on your own, based on the code I suggested.

In order to create your own certificate, you can use your favourite tools but I will suggest you OpenSSL. You can download it from the following address:

Before installing OpenSSL, assure that you have installed the proper version of Visual C++ 2008 Redistributables. Once you have installed these libraries, you can proceed by installing OpenSSL.

Creating Certificates

Before creating a certificate, we need to generate the private key. Achieving this with OpenSSL is really simple. Open your command prompt window and get yourself into the bin directory under the folder where you installed OpenSSL (usually C:\Program Files\OpenSSL).

Now execute the following command:

opensslgenrsa 1024 >private.key

Once executed, you should see something like this:

Check in that folder, there should be a new file called private.key. It is the RSA key on which the certificate will be based. Now let’s create the certificate. Execute the following command.

opensslreq -new -x509 -nodes -sha1 -days 1100 -key private.key> public.cer

As soon as you hit enter, you will be prompted for a couple of questions about the details with whom the certificates attributes will be populated. Once finished, you’ll see the following:

Congratulations! Your certificate is done!

Certificate is represented as Base64 encoded DER certificate, enclosed between “—–BEGIN CERTIFICATE—–” and “—–END CERTIFICATE—–”. Same is for the RSA signature key. For more details, consult:

Now, we can use a PKCS format file container in order to store both, public certificate and private key. This is not the goal of our project, but for the completeness I’ll show you how to do that via OpenSSL.

With the following command, you will create the required:

openssl pkcs12 -export -in public.cer -inkeyprivate.key –out cert_key.p12

After executing this command, you will get prompted about the Export password, this password will be used to encrypt your private key, so make it complex and unique.

Now you can load it to the X509Certificate2object:

string certificatePath = @"cert_key.p12";
string certificatePassword = "password";
X509Certificate2 clientCertificate = new X509Certificate2(certificatePath, certificatePassword);

You can download the source code here.

In the next steps, we will replicate this behaviour directly from code.
continue…

Certificates to DB and Back – Part 2

This is a second part of the article, if you missed the previous post you can find it here:
Certificates to DB and Back – Part  1

Table of contents

Certificates to DB and Back – Part 1

  1. Introduction
  2. Getting started
  3. Creating Certificates

Certificates to DB and Back – Part 2

  1. Loading the Certificate
  2. Decoding RSA Private Key
  3. Creating the X509Certificate2
  4. Demo Application
  5. Further Improvements
  6. License

Loading the Certificate

If only a certificate is a problem, X509Certificate2 class will do the job. With the following code, you can simply load the certificate:

string certificatePath = @"public.cer";
X509Certificate2 clientCertificate = new X509Certificate2(certificatePath);

Or if you want to load it directly from the string:

string publicCertString = "-----BEGIN CERTIFICATE----- MCIISFSDFEESd etc. example";
X509Certificate2 clientCertificate = 
	new X509Certificate2(Encoding.UTF8.GetBytes(publicCertString));

Now, the framework offers already much. But this is not enough. If we want to load a certificate from a certificate file, we can do it. Otherwise, if we want to load the certificate and private key from the pfx or pkcs12 container, we can do it. But what if we want to load dynamically the certificate and private key, choose the encryption password and use our object? The X509Certificate2 doesn’t offer an overload of constructor like X509Certificate2(string certFileName, string privateKeyFile, string password) or the similar overload that will accept a byte array in order to correctly initialize this object. In this case, we need to do some dirty work. So let’s do it.

Decoding RSA Private Key

I will not get into details on how the RSA key is decoded, you have the source code plain of comments, so if interested, read it, it says more than a thousand words. In order to decode the private key, we will use DecodeRsaPrivateKey method which will return RSACryptoServiceProvider instance representing our private key.

Creating the X509Certificate2

As described in the previous chapters, I will use the default X509Certificate2 constructor in order to create the certificate. Afterwards, I will assign the decoded RSA private key as RSACryptoServiceProvider to the X509Certificate2 instance property PrivateKey. If everything went well, we will have the proper instance of X509Certificate2 certificate container, containing both, the certificate and the key, encoded with chosen password. Here is the code sample:

byte[] certBuffer = Helpers.GetBytesFromPEM(publicCert, PemStringType.Certificate);
byte[] keyBuffer  = Helpers.GetBytesFromPEM(privateKey, PemStringType.RsaPrivateKey);

X509Certificate2 certificate = new X509Certificate2(certBuffer, password);

RSACryptoServiceProvider prov = Crypto.DecodeRsaPrivateKey(keyBuffer);
certificate.PrivateKey = prov;

I forgot to mention the helper method GetBytesFromPEM, which “cleans” the string from header and footer information.

Demo Application

In the demo application, you can see how to load the described components, create the X509 certificate, persist and reload everything. The interface is just an example, because that is rudimental, simple and incomplete. But hey, this is a demo, and for a demo is already too much! The important stuff is under the hood, that’s in what you should be interested.

Here is a screenshot:

Further Improvements

How to improve this code? Well test it, I have some doubts about creating Key Containers, and I will test it too. Any new discovery will be promptly noticed and the article will be updated.

Other things that are on my mind are creating the extension methods to the X509Certificate2 in order to make code look simpler and cleaner.

Perhaps including these methods in your applications’ framework cryptography library? Any idea is welcome so if you have any, feel free to contact me or to post the comment.

License

The idea and realisation are made completely by the author. The method DecodeRsaPrivateKey and all the code used inside were kindly provided by Mr Dan Maser; Dan, thanks for providing the code and for all the support. The way that RSACryptoServiceProvider is initialized is code written by me in order to overcome the encountered problems with .NET 4.0, all the traits-based size stuff and byte alignment are written by Dan Maser, but, speaking with Dan, we came to the following. The core of the ASN.1 parsing code came from a stackoverflow.com article. After analysing the code and after doing further research, I came across what I believe to be its original source, which is http://www.jensign.com/opensslkey/opensslkey.cs. The top of that file does include a standard copyright line “Copyright (C) 2008 JavaScience Consulting”. I didn’t knowingly use any copyrighted information when I originally wrote the code, but now it’s kind of fuzzy. There’s no direct indication I can find about any limitations on using or modifying the code directly on that website. At the bottom of http://www.jensign.com/JavaScience/cryptoutils/index.html, it does say “NOTE: These utilities and sample code are made available as-is with no support or guarantee of performance. They are intended to demonstrate specific technical implementation details with minimal error checking. Use at your own risk.” which does seem to directly suggest that one can use the published utilities, at albeit one’s own risk.

However, I think it is ok, but I guess you never know with the lawyers. If you find out that this code breaks the law, please contact me promptly and all code, as well as the article, will be removed.

If this is not the case, the article, along with any associated source code and files, is licensed under Microsoft Public License (MS-PL).

Joyful programming!

A Simple ASP.NET Flickr Application – Part 1

Foreword

I wrote this article back in 2010. This was my first programming article written in English. Actually it was more a need then a pleasure at the time as I was searching for my first employment here in The Netherlands and one of the companies that at whom I applied, requested a test that at the end with just a bit of extra effort I transformed in this article. As I suppose lately my English got better re-reading my own words make me wish to rewrite many of them, however it will be silly and great waste of time, so I will leave all the text just as it is in it’s original. In order to make it easier to read I will also split the article in n parts.

Let’s keep the conversation going! If any, just comment and will do my best to provide an answer to your doubts.

Download source code – 145 KB

Table of contents

A Simple ASP.NET Flickr Application – Part 1

  1. Introduction
  2. Getting started
  3. The web.config

A Simple ASP.NET Flickr Application – Part 2

  1. Constructing the page
  2. The make up
  3. The result
  4. Notes

Introduction

First of all, I want to thank Sam Judson who created a very useful project and that he shared it to us all. Without his effort, this article will be much longer and tedious. So thanks to him and other people who collaborated on that project. All project details and downloads can be found at this address: http://flickrnet.codeplex.com/.

I also used a light-weight, customizable lightbox plug-in for jQuery 1.3 and 1.4, called ColorBox, for adding a nice effect on the image preview. ColorBox is written by Jack Moore, and thanks to him too; you can find more details here: http://colorpowered.com/colorbox/. The version I’m using is 1.3.3, but you can update your projects with latest versions if you wish. More details about the version history can be obtained here: http://colorpowered.com/colorbox/core/README.

Another essential thing in order to use a Flickr API is creating your own API key and your secret key. If you already have a Yahoo! account, it will be quite simple; otherwise, you should create one. In both cases, you can start from here: http://www.flickr.com/services/api/keys/. For more details about the Flick API, visit http://www.flickr.com/services/api/.

Note that the API key and secret key used in the sample application are fake, they will not work. You’ll get an error message when executing the application. So you should change them, in the web.config, with data you got from Flicker! (Haven’t you created your own key? Bad bad, go to http://www.flickr.com/services/api/keys/ .)

Getting started

Start by creating an empty ASP.NET Web Site. I used Visual Studio 2010 Ultimate, but the same can be accomplished with Visual Studio Express as it can be done with previous versions of Visual studio such as 2008 or 2005.

Using the code

Let’s write some code. Add to your project an ASP.NET folder App_Code and create a new class and call it FlickrBLL. This is the code to be added:

using System;
using System.ComponentModel;
using System.Configuration;
using FlickrNet;

namespace Infrastructure.BLL
{
    ///<summary> 
    /// Helper class for confortable pagining and binding
    /// </summary>
    [DataObject(true)]
    public class FlickrBLL
    {
        [DataObjectMethodAttribute(DataObjectMethodType.Select, true)]
        public static PhotosetPhotoCollection GetPagedSet(string setId,
                      int maximumRows, int startRowIndex)
        {
            Flickr flickr = new Flickr(ConfigurationManager.AppSettings["apiKey"],
                ConfigurationManager.AppSettings["shardSecret"]);
            PhotosetPhotoCollection photos = flickr.PhotosetsGetPhotos(setId, GetPageIndex(
                startRowIndex, maximumRows) + 1, maximumRows);

            return photos;
        }

        public static int GetPagedSetCount(string setId)
        {
            Flickr flickr = new Flickr(ConfigurationManager.AppSettings["apiKey"],
                ConfigurationManager.AppSettings["shardSecret"]);
            Photoset set = flickr.PhotosetsGetInfo(setId);
            return set.NumberOfPhotos;
        }

        [DataObjectMethodAttribute(DataObjectMethodType.Select, false)]
        public static PhotosetCollection GetPhotoSetsByUser(string userId)
        {
            Flickr flickr = new Flickr(ConfigurationManager.AppSettings["apiKey"],
                ConfigurationManager.AppSettings["shardSecret"]);

            return flickr.PhotosetsGetList(userId);
        }

        protected static int GetPageIndex(int startRowIndex, int maximumRows)
        {
            if (maximumRows                 return 0;
            else
                return (int)Math.Floor((double)startRowIndex / (double)maximumRows);
        }
    }
}

Let’s analyze some of these methods.

The Fickr.Net library method PhotosetsGetPhotos expects the page index, and not the index of the first record to retrieve, so I created the GetPageIndex helper method for the conversion.

The method GetPhotoSetsByUser returns the result of PhotosetsGetList. Note that this function doesn’t expect as parameter the Flickr user name, but the user id. You can retrieve this data using other methods integrated into the Flicker.Net API or using websites such as http://www.xflickr.com/fusr/.

Don’t get scared by the GetPagedSet method attribute. Components such as the ObjectDataSource control and the ObjectDataSourceDesigner class examine the values of this attribute, if present, to help determine which data method to call at run time. It isn’t necessary, but it simplifies the work. The same attribute is used to indicate that this class is a data object ([DataObject(true)]). I decides to make use of pagination in this example, and it was easy because the API already provides this functionality. The method simply calls the PhotosetsGetPhotos available in the Flicker.Net library, and uses the opportune overload.

The Count method needed for paging uses GetPagedSetCount, which gets the requested set’s info and returns the number of items in the set.

The web.config

In order to acquire authentication data and other parameters that may vary, we need to create an appSettings section in our web.config.

<appSettings>
  <add key="apiKey" value="6370a3f6c4f1c031afd636247a648385"/>
  <add key="shardSecret" value="42cf8c1g85e6d3b2"/>
  <add key="defaultUser" value="10734446@N06"/>
  <add key="defaultPageSize" value="44"/>
</appSettings>

apiKey and sharedSecret are dummy values. You need to register and replace them to obtain a full functionality, but you can still use the default user and the default page size. If you are using a different layout, you can change the number of photos shown on each page, by simply varying this value.

continue…

Download source code – 145 KB

A Simple ASP.NET Flickr Application – Part 2

This is a second part of the article, if you missed the previous post you can find it here:
A Simple ASP.NET Flickr Application – Part 1

To download the source code click here.

Table of contents

A Simple ASP.NET Flickr Application – Part 1

  1. Introduction
  2. Getting started
  3. The web.config

A Simple ASP.NET Flickr Application – Part 2

  1. Constructing the page
  2. The make up
  3. The result
  4. Notes

Constructing the page

Add a new web form to your project and place inside it a DropDownList. After that, place inside the page an ObjectDataSurce control. For this ObjectDataSurce control, you can use the Designer and the Smart Task for making things quicker.

<p>
    List of default user sets:
    <br />
    <asp:DropDownList ID="ddlSets" runat="server" 
        AutoPostBack="True" DataSourceID="odsSets"
        DataTextField="Title" DataValueField="PhotosetId" 
        Height="21px" Width="450px"
        OnSelectedIndexChanged="ddlSets_SelectedIndexChanged">
    </asp:DropDownList>
</p>
<asp:ObjectDataSource ID="odsSets" runat="server" 
        OldValuesParameterFormatString="original_{0}"
        OnSelecting="odsSets_Selecting" 
        SelectMethod="GetPhotoSetsByUser" 
        TypeName="Infrastructure.BLL.FlickrBLL">
    <SelectParameters>
        <asp:Parameter Name="userId" Type="String" />
    </SelectParameters>
</asp:ObjectDataSource>

As you can see, our method is expecting a value so it can properly retrieve the data. As our user ID is stored in theweb.config, this is the approach for passing a requested argument:

protected void odsSets_Selecting(object sender, ObjectDataSourceSelectingEventArgs e)
{
   e.InputParameters["userId"] = ConfigurationManager.AppSettings["defaultUser"].ToString();
}

Intercept the Selecting event and manually set the expected parameter. I’m getting it from the web.config; you can choose any other way, e.g.: using another control in the page (if you create a user search). If you are inexperienced, you may be wondering how I got the DataTextField and DataValueField values. Simple, they are properties of the Photoset class, which is an element of PhotosetCollection, that is our called method return type. To explore other properties, create an instance of the Photoset class, select it, and hit F12.

Now I will add a ListView control in order to list the data that is the outcome of my query.

<asp:ListView ID="lvImages" runat="server" DataSourceID="odsPhotos">
    <EmptyDataTemplate>
        <span>No data was returned.</span>
    </EmptyDataTemplate>
    <ItemTemplate>
        <a href="<%# Eval("MediumUrl") %>" rel="images" title="<%# Eval("Title") %>">
            <img alt="" src="<%# Eval("SquareThumbnailUrl") %>" />
	</a> 
	</ItemTemplate>
    <LayoutTemplate>
        <div id="itemPlaceholderContainer" runat="server" style="">
            <span runat="server" id="itemPlaceholder" />
        </div>
            <asp:DataPager ID="DataPager1" runat="server" PageSize="<%$ appSettings:defaultPageSize %>">
                <Fields>
                    <asp:NextPreviousPagerField ButtonType="Image" ShowFirstPageButton="true" ShowNextPageButton="false"
                        ShowPreviousPageButton="true" FirstPageImageUrl="~/images/first.gif" PreviousPageImageUrl="~/images/previous.gif" />
                    <asp:TemplatePagerField>
                        <PagerTemplate>
                            Page
                            <asp:Label runat="server" ID="labelCurrentPage" 
		Text="<%# Container.TotalRowCount > 0 ? (Container.StartRowIndex / Container.PageSize) + 1 : 0 %>" />
                            of
                            <asp:Label runat="server" ID="labelTotalPages" 
                Text="<%#  Math.Ceiling ((double)Container.TotalRowCount / Container.PageSize) %>" />
                        </PagerTemplate>
                    </asp:TemplatePagerField>
                    <asp:NextPreviousPagerField ButtonType="Image" ShowLastPageButton="true" ShowNextPageButton="true"
                        ShowPreviousPageButton="false" LastPageImageUrl="~/images/last.gif" NextPageImageUrl="~/images/next.gif" />
                    <asp:TemplatePagerField>
                        <PagerTemplate>
                            <br />
                            Total Pictures in this set:
                            <asp:Label runat="server" ID="labelTotalPictures" Text="<%#  (double)Container.TotalRowCount %>" />
                        </PagerTemplate>
                    </asp:TemplatePagerField>
                </Fields>
            </asp:DataPager>
    </LayoutTemplate>
</asp:ListView>

Note that I specified a very simple ItemTemplate; the code speaks for itself. The important thing to note is that inside the layout template, there is a DataPager control. When the DataPager is inside a ListView control, setting the PagedControlID of the DataPager isn’t necessary. The container ListView is automatically associated as the paged control.

Obviously, this will not work without a data source, so here we go, an ObjectDataSource is served.

<asp:ObjectDataSource ID="odsPhotos" runat="server" 
        EnablePaging="True" OldValuesParameterFormatString="original_{0}"
        SelectCountMethod="GetPagedSetCount" 
        SelectMethod="GetPagedSet" TypeName="Infrastructure.BLL.FlickrBLL"
        OnSelecting="odsPhotos_Selecting">
    <SelectParameters>
        <asp:ControlParameter ControlID="ddlSets" 
            DefaultValue="0" Name="setId" PropertyName="SelectedValue"
            Type="String" />
    </SelectParameters>
</asp:ObjectDataSource>

You can use Designer and the Smart Task, but be aware that a guided procedure will add the parameters that are used for paging and the app will not work correctly. Take a look at the properties that enable paging and the count method.

Now we need to set a maximum rows property, but as this property is specified in the web.config, we will do it from code.

protected void odsPhotos_Selecting(object sender, ObjectDataSourceSelectingEventArgs e)
{
   e.Arguments.MaximumRows =
     int.Parse(ConfigurationManager.AppSettings["defaultPageSize"]);
}

That’s almost it, but there are some small problems, like viewing the second page of a set and then changing the set, setting one that has just one page. Badly sad, more simply, at each set change, we need to reset the page counter.

protected void ddlSets_SelectedIndexChanged(object sender, EventArgs e)
{
    DataPager pgr = lvImages.FindControl("DataPager1") as DataPager;
    if (pgr != null && lvImages.Items.Count != pgr.TotalRowCount)
    {
        pgr.SetPageProperties(0, pgr.MaximumRows, false);
    }
}

Now, that’s it, and everything should work perfectly!

AJAXing

Let’s get rid of postbacks. Add on the page a ScriptManager and make the following changes:

Surround the ListView with the following code:

<asp:UpdatePanel ID="upImages" runat="server" style="text-align: center;">
    <ContentTemplate>
        // my list view
    </ContentTemplate>
    <Triggers>
        <asp:AsyncPostBackTrigger ControlID="ddlSets" />
    </Triggers>
</asp:UpdatePanel>

As the DropDownList is outside of the ListView, we need to indicate that even that control triggers a postback and that should be managed properly. Can’t be simpler!

The make up

I said before that we will use a lighbox plug-in called Colorbox. So let’s set it up. First of all, put all the images that are coming with this plug-in into the images directory, then do the same for the CSS style. Open the CSS file that you just copied, and check the image paths. Pay attention to comments about IE workarounds and the connected paths. Copy the necessary scripts to a script folder and then be ready for some code. We should link our scripts and the CSS file, so in the page head, add the following:

<script type="text/javascript" src="scripts/jquery.min.js"></script>
<script type="text/javascript" src="scripts/jquery.colorbox-min.js"></script>

Once we are done, we need to create an instance of Colorbox in order for this to work. As the JavaScript is no longer “bound” after a callback made via UpdatePanel, we need to reinitialize our Colorbox.

<script type="text/javascript">// <![CDATA[
        $(document).ready(function () {
            $("a[rel='images']").colorbox({ transition: "fade" });
        });

        function pageLoad(sender, args) {
            if (args.get_isPartialLoad()) {
                $("a[rel='images']").colorbox({ transition: "fade" });
            }
        }
// ]]></script>

For further information about the light box, and how you can get more from this practical plug-in, refer to the Colorbox webpage.

The result

In the end, by using a style that was automatically created by VS2010, this is the result:

FlickrPreview1

Here is a bit more detailed view:

FlickrPreview2

Notes

The default user for Filckr in the demo is actually an active and very nice Flickr profile. It’s my friend’s profile, mind_in_motion, so if you like nice photos, check it out. Thanks Jean Claude!  I’ll try to answer all of your questions, if any, just post a comment.

Joyful programming!

Afterword

This article was initially published on CodeProject.com and if you prefer you can still consult it there. No changes to the code or text are made.

Download source code – 145 KB