Monday 26 April 2010

Unit Testing: Accessing AppSettings within the context of the test


I recently had an issue with my tests failing, I realised it was because when using my particular gtest suite, the debug files are not stored within bin\Debug, but rather somewhere else; and the configuration files are not being copied!

This was quite dramatic as my DB connection strings are stored as settings, and needed these to produce tests against the data. The simpliest way to solve this problem is to add a post-build event to the test project. This psot build event will copy the configuration file to the target destination. The testing suite will then pick up these settings as and when requested.

1. Right click and properties on the test project
2. Click Build Events Tab.
3. Add the following post build event...
copy /Y "$(ProjectDir)app.config" "$(TargetPath).config"



This will ensure your config file is copied to its destination, where your test suite (I.e. NUnit can pick it up).

Friday 23 April 2010

JQuery: Select elements containing an attribute


Here's a simply JQuery snippit that will select all elements using the selector which contain a specified attribute...

This will select all table rows (TR's) of a table with an ID of 'tblTest' that have an attribute 'uniqueid'.
$("table#tblTest tr[uniqueid]")

Javascript: Add/Remove classes from elements


Here's a simple way of achiving this without any bloating libraries (I.e. JQuery etc.)
There is also a 'HasClass' method, which becomes quite useful.

function hasClass(ele, cls) {
    return ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'));
}
function addClass(ele, cls) {
    if (!this.hasClass(ele, cls)) ele.className += " " + cls;
}
function removeClass(ele, cls) {
    if (hasClass(ele, cls)) {
        var reg = new RegExp('(\\s|^)' + cls + '(\\s|$)');
        ele.className = ele.className.replace(reg, ' ');
    }
}

Wednesday 21 April 2010

ASP.NET: Using FindControl Recursively.


FindControl can be a bit of a pain, especially as it doesn't contain recursive support; that being, you need to know the direct parent of an object before you can start your search.

I have attached a method which is useful, especially when creating generic methods for groups of objects where the parent is unknown.

        /// <summary>
        /// Finds a Control recursively. Note finds the first match and exists
        /// </summary>
        /// <param name="RootControl">Root control to start looking in.</param>
        /// <param name="ControlToFind">The control to find within it recursively.</param>
        /// <returns>The control if it exists.</returns>
        public Control FindControlRecursive(Control RootControl, string ControlToFind)
        {
            if (RootControl.ID == ControlToFind)
                return RootControl;
 
            foreach (Control Ctl in RootControl.Controls)
            {
                Control FoundCtl = FindControlRecursive(Ctl, ControlToFind);
                if (FoundCtl != null)
                    return FoundCtl;
            }
 
            return null;
        }

Wednesday 14 April 2010

ASP.NET GridView: Adding Sorting and Paging with Business Object Binding


So recently, I was given the challenge of binding a complex business object to a GridView... I didn't think it would be tricky, but I did find a few niggles along the way. I thought I would share them so anybody else attempting to do this can pick up some lessons learned. The following is my implementation of this task...

This example displays a list of courses. Each course has an ID, a name and a production status. The production status' will be rendered as images within the gridview. Below is the designer structure of the grid..

        <asp:GridView ID="grdBantyGridView"
                    CssClass="gridView"
                    rowstyle-borderstyle="none"
                    GridLines="None" BorderStyle="None"
                    RowStyle-CssClass="row"
                    runat="server"
                    ShowFooter="True"
                    AutoGenerateColumns="False"
                    ShowHeader="True"
                    AlternatingRowStyle-CssClass="alternate"
                    AllowSorting="True"
                    UseAccessibleHeader="True"
                    onrowdatabound="grdBantyGridView_RowDataBound" 
                    onsorting="grdBantyGridView_Sorting"
                    OnPageIndexChanging="grdBantyGridView_PageIndexChanging" 
                    AllowPaging="True"
                    PageSize="10"
                    PagerSettings-Mode="NumericFirstLast"
                    PagerSettings-PageButtonCount="10"
                    DataKeyNames="ID" EnableViewState="False"
                    >
            <HeaderStyle CssClass="gridViewHeader" />
            <RowStyle BorderStyle="None" CssClass="row" />
            <Columns>
                <asp:BoundField DataField="ID" Visible="False" />
                <asp:TemplateField AccessibleHeaderText="Name" HeaderText="Name" 
                    SortExpression="Name">
                    <ItemTemplate>
                        <asp:Label ID="lblName" runat="server" 
                            Text='<%# Eval("Name") %>'></asp:Label>
                    </ItemTemplate>
                </asp:TemplateField>
                <asp:TemplateField AccessibleHeaderText="Description" HeaderText="Description" 
                    SortExpression="Description">
                    <ItemTemplate>
                        <asp:Label ID="lblDescription" runat="server" 
                            Text='<%# Eval("Description") %>'></asp:Label>
                    </ItemTemplate>
                </asp:TemplateField>
                <asp:TemplateField AccessibleHeaderText="Status" HeaderText="Status" 
                    SortExpression="Status">
                    <ItemTemplate>
                        <asp:Image ID="imgStatus" runat="server" 
                            ImageUrl='<%# GetCourseStatusUrl((Banty.Library.DTO.CourseStatus)Eval("Status")) %>' />
                    </ItemTemplate>
                </asp:TemplateField>
            </Columns>
            <FooterStyle CssClass="gridViewFooter" />
            <EmptyDataTemplate>
            No data available.
            </EmptyDataTemplate>
        <PagerStyle height="5px" verticalalign="Bottom" horizontalalign="Center" />
            <AlternatingRowStyle CssClass="alternate" />
    </asp:GridView>



Things to note:
AutoGenerateColumns - This gives us more flexibility over what columns appear when we bind out business objects.
AllowSorting - Allows the sorting, but because we have disabled AutoGenerateColumns, we need to override the 'onsorting' event.
AllowPaging - Allow the paging. We must override the 'OnPageIndexChanging' event to make this work.
DataKeyNames - Set this so that we can retrieve each row during the 'onrowdatabound' event. This will store a datakey against each row. I am using this to enable javascript highlights of rows in this example.

For each column in the Gridview definition, be sure to add a SortExpression value. This helps us in the code-behind to identify while column is requesting the sort.

The above points are key to creating this pagable and sortable grid, and must be taken into consideration... Lets now have a look at the code behind and how we fit this all together...


        private List<Banty.Library.DTO.Course> courses = null;
 
        #region >>> Properties
 
        public List<Banty.Library.DTO.Course> Courses { get { return this.courses; } set { this.courses = value; } }
 
        #endregion
 
        protected void Page_Load(object sender, EventArgs e)
        {
        }
 
        /// <summary>
        /// Initialises the user control.
        /// </summary>
        public void InitControl()
        {
            if (this.Courses != null)
            {
                this.SetDataSource(true);
 
                pnlNoData.Visible = false;
                pnlGridView.Visible = true;
            }
            else
            {
                pnlNoData.Visible = true;
                pnlGridView.Visible = false;
            }
        }
 
        /// <summary>
        /// Sets the datasource to the gridview.
        /// </summary>
        private List<Banty.Library.DTO.Course> SetDataSource(bool dataBind)
        {
            // Bind apps to the grid view.
            this.grdBantyGridView.DataSource = this.Courses;
 
            // Bind if requested
            if (dataBind)
                this.grdBantyGridView.DataBind();
 
            return this.Courses;
        }
 
        /// <summary>
        /// Performs actions as the row data is bound.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void grdBantyGridView_RowDataBound(object sender, GridViewRowEventArgs e)
        {
            if (e.Row.RowType == DataControlRowType.DataRow)
            {
                int uniqueID = 0;
                if (Int32.TryParse(this.grdBantyGridView.DataKeys[e.Row.RowIndex].Value.ToString(), out uniqueID))
                {
                    // Add applicationID to row
                    e.Row.Attributes["uniqueid"] = uniqueID.ToString();
                }
 
                // So that we can distinguish rows
                e.Row.Attributes.Add("OnMouseUp", "grdBantyGridView_OnMouseUp(" + e.Row.ClientID + ");");
            }
        }
 
        /// <summary>
        /// Provides an image URL for value.
        /// </summary>
        /// <param name="input">data value</param>
        /// <returns>A URL to the value icon</returns>
        protected string GetCourseStatusUrl(CourseStatus input)
        {
            string result = string.Empty;
 
            switch (input.ID)
            {
                case 1:
                    result = "../images/icon-no.gif";
                    break;
                case 2:
                    result = "../images/icon-yes.gif";
                    break;
                case 3:
                    result = "../images/icon-verified.gif";
                    break;
            }
 
            return result;
        }
 
        /// <summary>
        /// Handles the sorting of the gridview columns
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void grdBantyGridView_Sorting(object sender, GridViewSortEventArgs e)
        {
            // Determine weather column is valid
            string[] validFields = new string[] { "Name", "Description", "Status"};
 
            // if we have a valid field to sort....
            if (validFields.Contains(e.SortExpression))
            {
                // Get Sort direction
                string sortDir = this.GetSortDirection(e.SortExpression);
                List<Banty.Library.DTO.Course> courses = this.SetDataSource(false);
 
                // Filter on sort expression...
                switch (e.SortExpression)
                {
                    case "Name":
                        if (sortDir.Equals("ASC"))
                            courses.Sort(delegate(Banty.Library.DTO.Course c1, Banty.Library.DTO.Course c2) { return c1.Name.CompareTo(c2.Name); });
                        else
                            courses.Sort(delegate(Banty.Library.DTO.Course c1, Banty.Library.DTO.Course c2) { return -c1.Name.CompareTo(c2.Name); });
                        break;
 
                    case "Description":
                        if (sortDir.Equals("ASC"))
                            courses.Sort(delegate(Banty.Library.DTO.Course c1, Banty.Library.DTO.Course c2) { return c1.Description.CompareTo(c2.Description); });
                        else
                            courses.Sort(delegate(Banty.Library.DTO.Course c1, Banty.Library.DTO.Course c2) { return -c1.Description.CompareTo(c2.Description); });
                        break;
 
                    case "Status":
                        if (sortDir.Equals("ASC"))
                            courses.Sort(delegate(Banty.Library.DTO.Course c1, Banty.Library.DTO.Course c2) { return c1.Status.Status.CompareTo(c2.Status.Status); });
                        else
                            courses.Sort(delegate(Banty.Library.DTO.Course c1, Banty.Library.DTO.Course c2) { return -c1.Status.Status.CompareTo(c2.Status.Status); });
                        break;
                }
 
                // Rebind the gridview
                this.grdBantyGridView.DataBind();
            }
        }
 
        /// <summary>
        /// Handle page changing on the grid.
        /// </summary>
        /// <param name="sender">This parameter is not used.</param>
        /// <param name="e">This parameter is not used.</param>
        protected void grdBantyGridView_PageIndexChanging(object sender, GridViewPageEventArgs e)
        {
            // Update data source
            this.SetDataSource(false);
 
            this.grdBantyGridView.PageIndex = e.NewPageIndex;
            this.grdBantyGridView.DataBind();
        }
 
        // This is sort the column, by default, asc.
        // Unless, the column has been sorted already...
        // then it will sort the column in the opposite direction.
        private string GetSortDirection(string column)
        {
            // By default, set the sort direction to ascending.
            string sortDirection = "ASC";
 
            // Retrieve the last column that was sorted.
            string sortExpression = ViewState["SortExpression"] as string;
 
            if (sortExpression != null)
            {
                // Check if the same column is being sorted.
                // Otherwise, the default value can be returned.
                if (sortExpression == column)
                {
                    string lastDirection = ViewState["SortDirection"] as string;
                    if ((lastDirection != null) && (lastDirection == "ASC"))
                    {
                        sortDirection = "DESC";
                    }
                }
            }
 
            // Save new values in ViewState.
            ViewState["SortDirection"] = sortDirection;
            ViewState["SortExpression"] = column;
 
            return sortDirection;
        }
 
    }



We can also add javascript to apply styles to the selected row
//
// Handles highlighting table rows on mouseup event.
//
function grdBantyGridView_OnMouseUp(row) {
 
    // Reset grid styles
    ResetGrid();
 
    // Add selected class
    row.setAttribute('class', 'selected');
}
 
function ResetGrid()
{
    // Remove selected from all rows that have a unique attribute...
    $("table.gridView tr[uniqueid]").removeClass("selected");
 
    // Now add the alternate style to every 2nd row...
    for (var i = 1; i < $("table.gridView tr[uniqueid]").length + 1; i++)
    {
        if (i % 2 == 0)
            $("table.gridView tr[uniqueid]")[i-1].setAttribute('class', 'alternate');
    }
}

ASP.NET: Server.Transfer vs. Response.Redirect


A common misconception is the difference between Server.Transfer and Response.Redirect in ASP.NET applications. Redirect and Transfer both cause a new page to be processed, but the interaction between the client (web browser) and server (ASP.NET) is different in each situation.

Redirect: A redirect is just a suggestion – it’s like saying to the client “Hey, you might want to look at this”. All you tell the client is the new URL to look at, and if they comply, they do a second request for the new URL.

If you want to pass state from the source page to the new page, you have to pass it either on the URL (such as a database key, or message string), or you can store it in the Session object (caveat: there may be more than one browser window, and they’ll all use the same session object).

e.g. Redirect to the new.aspx page, passing an ID on the query string. "true" stops processing the current page:

Response.Redirect("new.aspx?id=34",true);


Transfer: A transfer happens without the client knowing – it’s the equivalent of a client requesting one page, but being given another. As far as the client knows, they are still visiting the original URL.

Sharing state between pages is much easier using Server.Transfer – you can put values into the Context.Items dictionary, which is similar to Session and Application, except that it lasts only for the current request. (search for HttpContext in MSDN). The page receiving postback can process data, store values in the Context, and then Transfer to a page that uses the values.

e.g. Store a message in the context dictionary, and transfer to the default.aspx page (which can then display the message):

Context.Items["Message"] = "Your password was changed successfully";
Server.Transfer("default.aspx");



Caveats:

Response.Redirect is more user-friendly, as the site visitor can bookmark the page that they are redirected to.
Transferred pages appear to the client as a different url than they really are. This means that things like relative links / image paths may not work if you transfer to a page from a different directory.
Server.Transfer has an optional parameter to pass the form data to the new page.
Since the release version, this no longer works, because the Viewstate now has more security by default (The EnableViewStateMac defaults to true), so the new page isn’t able to access the form data. You can still access the values of the original page in the new page, by requesting the original handler:

Page originalPage = (Page)Context.Handler;
TextBox textBox1 = (TextBox)originalPage.FindControl("textBox1"); 

Adding Song Name to the Result Screen in Stepmania


This enables you have add the name of the song to the results screen... it swings in from the top after a 3 second delay. The command attributes are sequential, which took me a few secs to figure out; so one event happens before the next in that order.


Add to "[Install]\Themes\[Theme Name]\BGAnimations\ScreenEvaluation overlay.xml"
Code Snippet
  1. <Layer Type="ActorFrame" OnCommand="x,SCREEN_CENTER_X;y,SCREEN_TOP-10;sleep,3;decelerate,0.3;addy,SCREEN_TOP+80" OffCommand="accelerate,0.3;addy,-80">
  2.     <children>
  3.         <BitmapText Font="Common Normal" Text="@GAMESTATE:GetCurrentSong():GetDisplayFullTitle()" OnCommand="zoom,0.4;diffuse,#FFFFFF;ztest,1" />
  4.     </children>
  5. </Layer>
End of Code Snippet


Ensure its added for both players by placing it out of the player condition tags (I.e. at the top or bottom of a default config file)

Tuesday 13 April 2010

GridView: Switch Images in cells depending upon bound data


It is quite common to have a scenario where your storing a boolean in the database, and at the front end, you would like to render this as a tick or a cross. This is a very simple example, but this can be built upon to enable more complex scenarios...


1. Setup your datagrid with a template column...
<asp:TemplateField HeaderText="Enabled" AccessibleHeaderText="Enabled">
<ItemTemplate>
    <asp:Image ID="imgEnabled" runat="server" ImageUrl='<%# GetImageUrl((bool)Eval("Enabled")) %>' />
</ItemTemplate>
</asp:TemplateField>


In this example, were using a simple boolean. These can be custom objects as long as they are casted here and fully qualified with the namespace; so that the objects can be identified and the method can be paired.

Now, in the code behind, we need to add the logic to output this image URL.
/// <summary>
/// Provides an image URL for an enabled status.
/// </summary>
/// <param name="input">Enabled value</param>
/// <returns>A URL to the enabled icon</returns>
protected string GetImageUrl(string enabled)
{
    string result = string.Empty;
 
    if (enabled)
        result = "../images/icon-tick.gif";
    else
        result = "../images/icon-cross.gif";
 
    return result;
}


You can see that this method returns a string, this is accepted by the ImageUrl attribute where we placed the Eval statement. cool huh?

The GridView 'grdNameHere' fired event PageIndexChanging which wasn't handled.


If you've used a GridView on an ASP.NET page and enabled paging, then you will probably be presented with this error message. The fix is simple, but why it does this without some sort of warning from the IDE is rather strange...

So... you have your grid view with paging enabled...

<asp:GridView ID="grdTest" runat="server" AutoGenerateColumns="False" ShowHeader="True" AllowPaging="True" UseAccessibleHeader="True">
 
...column definitions here...
 
</asp:GridView>


We need to override the 'PageIndexChanging' event now, and add some custom code to cater for our paging.


<asp:GridView ID="grdTest" runat="server" AutoGenerateColumns="False" ShowHeader="True" AllowPaging="True" UseAccessibleHeader="True" OnPageIndexChanging="grdTest_PageIndexChanging">
 
...column definitions here...
 
</asp:GridView>


code behind...
/// <summary>
/// Handle page changing on the grid.
/// </summary>
/// <param name="sender">This parameter is not used.</param>
/// <param name="e">This parameter is not used.</param>
protected void grdTest_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
     // Update applications
     this.DataBindingMethod();  // Change this to yours!!
 
     this.grdTest.PageIndex = e.NewPageIndex;
     this.grdTest.DataBind();
}



Note: This data bind method (DataBindingMethod() in this case) must be where your grid is initially bound to.

Wednesday 7 April 2010

ASP.Net CustomValidator and CompareValidator - Validating Empty Controls


A lot of people have had trouble with validating empty controls when using the Custom Validator and Compare Validators within the ASP.NET framework.

As of .NET 2.0, an Attribute has been added to each control called "ValidateEmptyText".

Add this to each control consumed and away you go!

TIP: How To Generate a Fully Qualified URL in ASP.NET (E.g., http://www.yourserver.com/folder/file.aspx)


Imagine that you've got an ASP.NET page that is generating an email message that needs to include links back to the website. Perhaps you're writing the next greatest online message board application and when someone replies to a thread you want to send out emails to the other thread participants indicating that a new message has been posted along with a link to view the just-posted message. You know that the URL to view a particular thread is, say, ~/Threads/View.aspx?ThreadId=threadId. But how do you turn that relative URL into an absolute URL like http://www.yourserver.com/Threads/View.aspx?ThreadId=threadId?

http://scottonwriting.net/sowblog/posts/14011.aspx