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

    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 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 GetUsers()
    {
        List users = new List();

        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 GetSomeProjects()
    {
        List projects = new List();

        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 GetSomeProjects2()
    {
        List projects = new List();

        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:


    
        
        
        
        
        
        
        
        
        
        
    

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 Users
{
    get
    {
        if (Session["Data"] == null)
            Session["Data"] = DataService.GetUsers();

        return (List)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 section then inside the newly created section a new one called . 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.



    
        
            
                
                
                
                
                
                
            
        
    

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


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:


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:


    
        
        
        
        
        
        
    
    

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


    
        
        
    

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:


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.


    
        
        
    
    

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.


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:


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:


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


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 values = (Dictionary)grid.JSProperties["cpStatus"];

        if (values.ContainsKey(e.VisibleIndex))
        {
            values[e.VisibleIndex] = status;
        }
        else
        {
            values.Add(e.VisibleIndex, status);
        }

        grid.JSProperties["cpStatus"] = values;
    }
    else
    {
        Dictionary values = new Dictionary();
        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:


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



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



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:


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:\Usersmario.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% AppDataLocal 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!

Securing access to the Cisco routers

This article is a translation of my original post written in Italian by the end of 2008 and published on areanetworking.it. As you may know, my working experience and specialized studies have provided me with knowledge and skills in the analysis, implementation, optimization, troubleshooting and documentation of LAN/WAN network systems with strong “hands on” technical knowledge. However in that period I was programming only and it was a long time that I didn’t seriously configured a router. One of my old customers commissioned a configuration of three routers, one in the main office and other two in branches. It was for me something new and challenging because I never before configured fully mashed VPN connection so I wanted to get a bit further and configure also other aspects in a more detailed way. I checked my previous configurations an no one of them satisfied me completely. So I pulled down all the documentation I found and started studying. The following lines are the result of that.

Let´s start, first thing that I did is to separate the virtual terminal lines in two groups, the first one will be reserved for the connections from external networks meanwhile the second will be used for the connections from internal networks. From the external networks will be only possible to connect via SSH where from the internal networks telnet will also be an option.
To achieve this I have given the following commands:

line vty 0 1
line vty 2 4

In this post I will not get in depth about SSH protocol, if you are interested in different version of the protocol, you will find many information’s by Googleing the keywords. If you yet do not know anything about it, it is a way to connect to the terminal where all the data exchanged between client and server are encoded. To achieve that let´s give the following commands:

line vty 0 1
    transport input ssh
line vty 2 4
     transport input telnet ssh

Now we need to make sure that we set up the hostname and the domain name, otherwise, generating the cryptography key will fail. If there is the default hostname set, it would be advisable to change it. Both can be set with the following commands:

hostname MyRouter
ip domain-name majcica.com

Following task is generating the cryptography key base on whom the SSH connection will be secured (will be used to encrypt the data).

crypto key generate rsa

Once you issue this command you will be asked to choose the key size. I will suggest you to leave the default value (512) which is more than enough for 99% of cases. When this operation is over, you will receive the message that SSH is activated and that the current version is 1.5. Unfortunately there is no SSH 1.5 and you may be confused, however this is the way Cisco indicates that SSH version 1 is active. You can always check the current active version by the following command:

show ip ssh

In case it says 1.99 it means that both SSH1 and SSH2 are enabled, meanwhile if the result is 2.0 it means that only the version 2 is enabled.

In order to prevent the access to a virtual terminal interfaces that accepts the telnet connection let’s suppose that our internal network is 10.10.10.024 (10.10.10.x network with a mask 255.255.255.0). Because of that we will create an access list that accepts the traffic only from internal network and block the traffic from all other sources. In order to achieve this is sufficient issuing the following command:

access-list 15 permit 10.10.10.0 0.0.0.255 log

and then apply this access list to a proper virtual terminal interface:

line vty 2 4
   access-class 15 in

If we try now to access the router via telnet from a network that is different than our internal, the request will time out. But if we try to access it from an external network via SSH the router will replay and we will be able to establish a connection.
Next step is setting the time out (60 seconds is a reasonable value), enabling SSH version 2, setting the interface from which route will replay to SSH request (outside interface, in our case are ATM0.1 – ADSL connection) and a maximum authentication retries.

ip ssh time-out 60
ip ssh authentication-retries 5
ip ssh source-interface ATM0.1
ip ssh version 2

Initially I tough that this is sufficient but once I configured logging, I found that, meanwhile testing, there was no trace about the who and when connected to the router. As I believe that this are information’s that we should log, I made a little research and found the following commands:

login on-failure log
login on-success log

First one enable logs on failed attempts meanwhile the second one enables the log on successful log in’s.
Till here everything was fine until couple of days later I consulted the log files and found that someone was attempting brute force log in attacks on my router. I needed a solution for this problem, and limiting the access via ACL was not an option as I often connect to the router via a dynamically obtained IP address. At the end I found that we can set a dynamic block for the users who fails authenticating a certain amount of time in a determinate time interval. Basically if we want to prevent that user receive a response from SSH service for 300 seconds in case he misses the password three times in 30 sec, we need to issue the following command:

login block-for 300 attempts 3 within 30

This for me was sufficient, and it seems a decent practice, however if you feel paranoid you can still change the default SSH port, apply restrictive ACL, etc.
Following you will find an example extract from syslog showing a brute force attack and the quite mode in action.

2009-01-27 08:28:48	Local7.Warning	10.10.10.254	81: 000086: Jan 27 08:28:47.028 Berlin: %SEC_LOGIN-4-LOGIN_FAILED:
Login failed [user: nagios] [Source: 210.192.123.204] [localport: 22] [Reason: Login Authentication Failed]
at 08:28:47 Berlin Tue Jan 27 2009

2009-01-27 08:28:53	Local7.Warning	10.10.10.254	82: 000087: Jan 27 08:28:52.194 Berlin: %SEC_LOGIN-4-LOGIN_FAILED:
Login failed [user: user] [Source: 210.192.123.204] [localport: 22] [Reason: Login Authentication Failed]
at 08:28:52 Berlin Tue Jan 27 2009

2009-01-27 08:28:57	Local7.Warning	10.10.10.254	83: 000088: Jan 27 08:28:57.169 Berlin: %SEC_LOGIN-4-LOGIN_FAILED:
Login failed [user: test] [Source: 210.192.123.204] [localport: 22] [Reason: Login Authentication Failed]
at 08:28:57 Berlin Tue Jan 27 2009

2009-01-27 08:28:57	Local7.Alert	10.10.10.254	84: 000089: Jan 27 08:28:57.169 Berlin: %SEC_LOGIN-1-QUIET_MODE_ON:
Still timeleft for watching failures is 19 secs, [user: test] [Source: 210.192.123.204] [localport: 22]
[Reason: Login Authentication Failed] [ACL: sl_def_acl] at 08:28:57 Berlin Tue Jan 27 2009

2009-01-27 08:33:58	Local7.Notice	10.10.10.254	85: 000104: Jan 27 08:33:57.083 Berlin: %SEC_LOGIN-5-QUIET_MODE_OFF:
Quiet Mode is OFF, because block period timed out at 08:33:57 Berlin Tue Jan 27 2009

I didn’t went in detail on all steps supposing that you have basic knowledge on how to operate on Cisco IOS. All the information’s I got from Cisco.com. Also this procedure where valid by the end of 2008 when the original article was written. However it should not be drastically changed in the newest version of IOS. If so, check the documentation on cisco.com and eventually post your question’s in the comments.

Joyful configuring!