Philip Hendry's Blog

December 23, 2011

Create a contents page with page numbers from html input using Websupergoo Abcpdf

Filed under: ASP.NET — philiphendry @ 9:25 am

I’ve created a report pdf from an html page output from our ASP.NET based product but I needed to change a contents list that was rendered as a hyperlink list on the page into a list of section headings and page number since the pdf was primarily for printing. The solution wasn’t immediately obvious but I’ve come up with the following which I was running in a unit test for simple quick prototyping. The key is using the HtmlOptions.AddTags property in ABCpdf which allows areas of the HTML to be retrieve during pdf rendering and modified.

 

   1: using System.Diagnostics;

   2: using System.IO;

   3: using System.Collections.Generic;

   4: using System.Linq;

   5: using Microsoft.VisualStudio.TestTools.UnitTesting;

   6: using WebSupergoo.ABCpdf8;

   7:  

   8: namespace TestPdfFormFields

   9: {

  10:    [TestClass]

  11:    public class PdfPrototyping

  12:    {

  13:       struct Tag

  14:       {

  15:          public string name;

  16:          public string rectString;

  17:          public int pagenumber;

  18:       }

  19:  

  20:          

  21:       [TestMethod]

  22:       public void testCreatingAPdfWithContentsPageFromHtml()

  23:       {

  24:          // Create Websupergoo ABCpdf document and set up the page size

  25:          var theDoc = new Doc();

  26:          theDoc.Rect.Inset(100, 100);

  27:          theDoc.Rect.Top = 700;

  28:  

  29:          // The following HtmlOption instructs ABCpdf to interpret the style tags 'abcpdf-tag-visible' and

  30:          // create a array of the id's take from the html and the rectangles representing the size of the 

  31:          // html element. 

  32:          theDoc.HtmlOptions.AddTags = true;

  33:  

  34:          // The basis of this solution therefore is to create placeholders where page numbers will be rendered

  35:          // in the contents page and back-fill them once we know where the sections/chapters have been rendered

  36:          // since it won't be known until ABCpdf has chained pages together.

  37:          var theID = theDoc.AddImageHtml(

  38:                   @"

  39:                      <h1>Contents</h1>

  40:                      <ul>

  41:                         <li><span id='contents1' style='abcpdf-tag-visible: true; width: 20px;'></span>.......First paragraph</li>

  42:                         <li><span id='contents2' style='abcpdf-tag-visible: true; width: 20px;'></span>.......Second paragraph</li>

  43:                         <li><span id='contents3' style='abcpdf-tag-visible: true; width: 20px;'></span>.......Third paragraph</li>

  44:                      </ul>

  45:                      <h1 id='heading1' style='abcpdf-tag-visible: true; page-break-before:always;'>Section One</h1>

  46:                      <h1 id='heading2' style='abcpdf-tag-visible: true; page-break-before:always;'>Section Two</h1>

  47:                      <h1 id='heading3' style='abcpdf-tag-visible: true; page-break-before:always;'>Section Three</h1>

  48:                   ");

  49:  

  50:          var tagCache = new List<Tag>();

  51:          var pagenumber = 1;

  52:          while (true)

  53:          {

  54:             // Fetch all the tags and rectangles and add them to a tagCache for the current theID. Chaining

  55:             // creates a new theID which will contain more tags to add

  56:             var tags = theDoc.HtmlOptions.GetTagIDs(theID);

  57:             var tagRects = theDoc.HtmlOptions.GetTagRects(theID);

  58:             tagCache.AddRange(tags.Select((t, i) => new Tag {name = t, pagenumber = pagenumber, rectString = tagRects[i].String}));

  59:  

  60:             if (!theDoc.Chainable(theID))

  61:                break;

  62:  

  63:             theDoc.Page = theDoc.AddPage();

  64:             theID = theDoc.AddImageToChain(theID);

  65:             pagenumber++;

  66:          }

  67:  

  68:          // Now we have a cache of all contents and heading tags we can iterate through the

  69:          // contents tags, find the smallest page number of the corresponding section then

  70:          // render the page number into the contents.

  71:          theDoc.HPos = 1.0;   // Right justify

  72:          theDoc.VPos = 0.5;   // Centre vertically

  73:          theDoc.FontSize = 8;

  74:          foreach (var tag in tagCache.Where(t => t.name.StartsWith("contents")))

  75:          {

  76:             var paragraphName = "heading" + tag.name.Substring("contents".Length);

  77:             var paragraphPageNumber = tagCache.Where(t => t.name == paragraphName).Select(t => t.pagenumber).Min();

  78:  

  79:             theDoc.PageNumber = tag.pagenumber;

  80:             theDoc.Rect.String = tag.rectString;

  81:             theDoc.AddText(paragraphPageNumber.ToString());

  82:          }

  83:  

  84:          // Now iterate through all the pages, add page numbers and flatten the layers.

  85:          theDoc.Rect.String = "100 70 500 150";

  86:          var pageCount = theDoc.PageCount;

  87:          for (var pageNumber = 1; pageNumber <= pageCount; pageNumber++)

  88:          {

  89:             theDoc.PageNumber = pageNumber;

  90:             theDoc.AddText("Page " + pageNumber + " of " + pageCount);

  91:             theDoc.Flatten();

  92:          }

  93:  

  94:          const string testFilename = @"c:\temp\HtmlOptionsGetTagRects.pdf";

  95:          if (File.Exists(testFilename))

  96:             File.Delete(testFilename);

  97:          theDoc.Save(testFilename);

  98:          Process.Start(testFilename);

  99:       }

 100:    }

 101: }

March 23, 2011

Configuring Event Log Permission for Asp.net applications

Filed under: ASP.NET, Security — philiphendry @ 2:40 pm

I’ve been having some problems with writing to a custom event log as well as the standard ‘Application’ event log where it failed with the following error :

    Stack Trace: 
    
    
    [Win32Exception (0x80004005): Access is denied]
    
    [InvalidOperationException: Cannot open log for source 'Application'. You may not have write access.]
    System.Diagnostics.EventLog.OpenForWrite(String currentMachineName) +1008783
    System.Diagnostics.EventLog.InternalWriteEvent(UInt32 eventID, UInt16 category, EventLogEntryType type, String[] strings, Byte[] rawData, String currentMachineName) +216
    System.Diagnostics.EventLog.WriteEntry(String message, EventLogEntryType type, Int32 eventID, Int16 category, Byte[] rawData) +264
    System.Diagnostics.EventLog.WriteEntry(String source, String message, EventLogEntryType type, Int32 eventID, Int16 category, Byte[] rawData) +87
    System.Diagnostics.EventLog.WriteEntry(String source, String message, EventLogEntryType type) +14
    ASP.test_testeventlogaccess_aspx.WriteToApplication(Object sender, EventArgs e) in c:\Inetpub\wwwroot\connect\Test\TestEventLogAccess.aspx:20
    System.Web.UI.WebControls.Button.OnClick(EventArgs e) +111
    System.Web.UI.WebControls.Button.RaisePostBackEvent(String eventArgument) +110
    System.Web.UI.WebControls.Button.System.Web.UI.IPostBackEventHandler.RaisePostBackEvent(String eventArgument) +10
    System.Web.UI.Page.RaisePostBackEvent(IPostBackEventHandler sourceControl, String eventArgument) +13
    System.Web.UI.Page.RaisePostBackEvent(NameValueCollection postData) +36
    System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +1565

What made this harder to figure was it worked in one environment but not another! The difference was impersonation was turned on in the web.config for the failing web site :


  <system.web>
    <identity impersonate="true" />
  </system.web>

This meant that the user being used to access the event log was not the AppPool identity but rather the ASP.NET IUSR_ identity (which has not been overridden with the optional username/password attributes above in the config above.)

Microsoft has a knowledge base article describing how to configure event log permissions but there were two extra steps I had to perform before I could add the IUSR identity to the event log permissions :

Fetching the SID for an account

The SDDL string that needs to be added according to the knowledge base article referenced above requires the SID for the IUSR account. There are a couple of ways to do this listed below. Either script block should be saved to a .vbs file and run as a parameter to cscript.exe from a command line :

    strComputer = "."
    Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
    Set objAccount = objWMIService.Get("Win32_UserAccount.Name='IUSR_<COMPUTER NAME>',Domain='<COMPUTER NAME>'")
    Wscript.Echo objAccount.SID
    
    
    On Error Resume Next
    strComputer = "."
    Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
    Set colItems = objWMIService.ExecQuery("Select * from Win32_UserAccount",,48)
    For Each objItem in colItems
        Wscript.Echo "AccountType: " & objItem.AccountType
        Wscript.Echo "Caption: " & objItem.Caption
        Wscript.Echo "Description: " & objItem.Description
        Wscript.Echo "Disabled: " & objItem.Disabled
        Wscript.Echo "Domain: " & objItem.Domain
        Wscript.Echo "FullName: " & objItem.FullName
        Wscript.Echo "InstallDate: " & objItem.InstallDate
        Wscript.Echo "Lockout: " & objItem.Lockout
        Wscript.Echo "Name: " & objItem.Name
        Wscript.Echo "PasswordChangeable: " & objItem.PasswordChangeable
        Wscript.Echo "PasswordExpires: " & objItem.PasswordExpires
        Wscript.Echo "PasswordRequired: " & objItem.PasswordRequired
        Wscript.Echo "SID: " & objItem.SID
        Wscript.Echo "SIDType: " & objItem.SIDType
        Wscript.Echo "Status: " & objItem.Status
    Next

Remove the IUSR Identity from the Guest Users Group

The last modification which is not mentioned in the knowledge base article is that the Guest Users group is already explicitly denied access to the event log (at least on my Windows Server 2003 machine) and therefore adding the SID for this account will not have an account. To solve this I removed the IUSR account from the Guest Users group and everything worked.

Debugging

As an aside to be able to test and debug this on both a production and test server I created an .aspx file with no code-behind with the following code :

    <%@ Page Language="C#" %>
    <%@ Import Namespace="System.Diagnostics"%>
    
    
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Test Event Log Access</title>
        
        <script runat="server">
        
        protected void WriteToMyEventLog(object sender, EventArgs e)
        {
            EventLog.WriteEntry("MyEventLog", "This is a test", EventLogEntryType.Error);
        }
    
        protected void WriteToApplication(object sender, EventArgs e)
        {
            EventLog.WriteEntry("Application", "This is a test", EventLogEntryType.Error);
        }        
        </script>
        
    </head>
    <body>
        <form id="form" runat="server">
        <div>
            <asp:Button runat="server" ID="btnWriteToMyEventLog" Text="Write to My Event Log" OnClick="WriteToMyEventLog" />
            <asp:Button runat="server" ID="btnWriteToApplication" Text="Write to Application" OnClick="WriteToApplication" />
        </div>
        </form>
    </body>
    </html>

February 10, 2011

Displaying ‘Unsaved Changes’ message for a web page using jQuery

Filed under: ASP.NET, jQuery, Web — philiphendry @ 10:48 am

I had a need to allow a user to cancel navigation to another page if changes to a form had not yet been saved – especially if they attempted to navigate to another page from the ever present menu. The code I used has a slight ‘hack’ which involves using the propertychange DOM event in IE rather than the change event which I would have usually expected.

Here’s the code that sets a global flag to indicate there’s an unsaved change :

var _changesMade = false;
$(document).ready(function() {

   $('form').bind($.browser.msie ? 'propertychange' : 'change', function() {
       _changesMade = true;
   });

   $(window).bind('beforeunload', function() {
       if (_changesMade)
           return 'There are unsaved changes which will be lost if you continue.';
   });
});

There are a couple of caveats. Any button that actually does the save will need a OnClientClick that sets the _changesMade to false to prevent the message from being displayed :

<asp:Button ID="btnSubmit" runat="server" Text="Submit" CssClass="button" OnClick="btnSubmit_Click" 
    OnClientClick="_changesMade=false; return true;" /> 

Also, if you have any code that causes the change event to fire (DOM manipulation after an Ajax call) then you might have to save the state of the _changesMade flag, make the DOM changes then reset the flag to it’s original value.

September 23, 2010

Strongly-typed ASP.NET Data-Binding

Filed under: ASP.NET — philiphendry @ 8:18 pm

Update : Just thought I’d better highlight something before I get shot down! I could, of course, early-bind to data by saying DataField=”<%#((Employee)Container.DataItem).Name%>” but below I was exploring ways to leverage code generation I had to make the syntax better – not sure I’ve done that of course, but the journey was fun!!

I was about to write the following code… again…

    <asp:GridView ID="grdSickness" runat="server" AutoGenerateColumns="false">
        <Columns>
            <asp:BoundField HeaderText="Name" DataField="EmployeeName" />
        </Columns>
    </asp:GridView>

…but it strikes me as very ‘yesterday’ to have an un-typed DataField property. If the entity changes that’s bound to this grid I’m not going to know about it until the grid is bound and that’s way too late for me!

What I really want to see is something like this :

      <asp:BoundField HeaderText="Name" DataField="<%=Entity.EmployeeName%>" />

Where the Entity object provides me with intellisense list of names. Now, this is by no means perfect and all I was looking for was a way to ‘inject’ the property name into the DataField attribute.

Code Generation

And the reason this comes so easily is because I’ve already written a code generator that creates a data access layer of strongly typed calls to stored procedures and uses the sql command ‘set fmtonly on’ to interpret the result sets of the stored procedures to generate entities that can be loaded into collections all fairly automatically. All I had to do was add four lines of code (and six more for the unit test) to pump out a static inner class into my entity that returns those property names. Here’s an example entity and one sproc wrapper that was generated :

    public partial class Employee : BaseEntity
    {
        private Employee() { }
        public String Name { get; private set; }
        public String Position { get; private set; }
        public static class ColumnNames
        {
            public static String Name { get { return "Name"; } }
            public static String Position { get { return "Position"; } }
        }
        public static Employee Load(IDataRecord record)
        {
            return new Employee
            {
                Name = GetNullableString(record, "EmployeeName"),
                Position = GetNullableString(record, "EmployeePosition")
            };
        }
        public static IEnumerable<Employee> LoadCollection(IDataReader reader)
        {
            return LoadCollection<Employee>(reader, Load);
        }
    }

    public partial class EmployeeDAL
    {
        public static DbDataReader spEmployeeSearch(bool? includeAuthorised)
        {
            Database db = DatabaseFactory.CreateDatabase("DefaultConnectionString");
            DbCommand command = db.GetStoredProcCommand("[spEmployeeSearch]");
            db.AddInParameter(command, "includeAuthorised", DbType.Boolean, includeAuthorised);
            return (DbDataReader)db.ExecuteReader(command);
        }
    }

The extra bit that I’ve added to this generated is the static inner class called ColumnNames. This, I thought meant I could write this :

<asp:BoundField HeaderText="Name" DataField="<%=Employee.ColumnNames.Name%>" />

Sideline : The code generator I’ve written uses a text template to integrate with Visual Studio which calls C# code I’ve written which then uses SQL Server SMO objects to connect to a database, iterate through the stored procedures and read extended properties which describe what to generate from them. I prefer keeping C# code in the text template to a minimum especially as I wanted the code generator to be fully unit tested!

Code Expressions

Whoops… but I forgot – you can’t but an expression in a WebControl attribute like that and I got this error at runtime :

{"A field or property with the name '<%Employee.ColumnNames.Name%>' was not found on the selected data source."}

Luck would have it I’ve already solved this one before. An extensibility point in ASP.NET allows me to create a new expression processor. It’s easier just to show the code and how to use it. Here’s the code :

namespace Utility.Web
{
    [ExpressionPrefix("Code")]
    public class CodeExpressionBuilder : ExpressionBuilder
    {
        public override CodeExpression GetCodeExpression(BoundPropertyEntry entry,
           object parsedData, ExpressionBuilderContext context)
        {
            return new CodeSnippetExpression(entry.Expression);
        }
    }
}

Which has to be registered in the web.config :

<?xml version="1.0"?>
<configuration>
    <system.web>
        <compilation>
            <expressionBuilders>
                <add expressionPrefix="Code" type="Utility.Web.CodeExpressionBuilder, Utility.Web"/>
            </expressionBuilders>
        </compilation>
    </system.web>
</configuration>

Now I can write the final version of my binding like this :

    <asp:BoundField HeaderText="Name" DataField="<%$ Code : Employee.ColumnNames.Name%>" />
    

That might seem like a lot of effort, but I now have a system whereby if I change any column in the result set of a stored procedure I’ll get a compile error where the entity is bound to a UI element and, to be honest, this blog post probably took longer to write!!

Oh, one caveat, I only bind directly to entities generated against a sproc for trivial CRUD like screens. Where it gets more complicated I would generate a model class and transform the entities into it in the same manner I would for ASP.NET MVC apps.

September 21, 2010

Always close Your HTML Elements Properly

Filed under: ASP.NET — philiphendry @ 9:16 am

I’ve just had a strange error trying to write a noddy ASP.NET page using Microsoft Ajax. The error itself was Microsoft JScript runtime error: Object required which basically tells you nothing. This error was being thrown in Sys$WebForms$PageRequestManager$_initializeInternal by the line trying to access this._form.onsubmit. It would appear _form is null! Looking up the call stack it originated from the Ajax initialize call : Sys.WebForms.PageRequestManager._initialize(‘scriptManager’, document.getElementById(‘form1′));. It would appear that the form element has not been closed yet and therefore the document.getElementById is searching for something that doesn’t exist.

Ok, that tells me nothing.

Thankfully I had an epiphany which worked (for once!) Spot the difference between this :

</title>
    <script type="text/javascript" language="javascript" src="GetResource.ashx?../lib/jquery/1.3.2/jquery-1.3.2.js" />
    <script type="text/javascript" language="javascript" src="GetResource.ashx?../lib/jqueryplugins/jquery.blockUI.js" />
    <script type="text/javascript" language="javascript" src="GetResource.ashx?../rms.utility.web.jqueryplugins/jquery.rms.ajaxprogress.js" />
</head>

and this :

<head runat="server">
    <title>jquery.rms.ajaxprogress.js Tests</title>
    <script type="text/javascript" language="javascript" src="GetResource.ashx?../lib/jquery/1.3.2/jquery-1.3.2.js"></script>
    <script type="text/javascript" language="javascript" src="GetResource.ashx?../lib/jqueryplugins/jquery.blockUI.js"></script>
    <script type="text/javascript" language="javascript" src="GetResource.ashx?../rms.utility.web.jqueryplugins/jquery.rms.ajaxprogress.js"></script>
</head>

In fact, it looks like my syntax highlighter I’m using in LiveWriter has spotted the difference too! Basically because I didn’t close the script element with a </script> IE only executed the first javascript fetch, Firefox wouldn’t execute any and Chrome worked!! Go figure!

September 14, 2010

Implementing a jQuery Browser history Plugin using microsoft Ajax History

Filed under: ASP.NET, jQuery, Uncategorized — philiphendry @ 8:31 pm

This article is a summary of the steps I took to produce a jQuery plugin that would create history points in the browsers history programmatically. In particular I wanted to create as simple an interface in the implementing page as possible and primarily needed to integrate with ASP.NET.

I will also describe my workaround for a bug in Microsoft Ajax ASP.NET 3.5 where the onNavigate event would not fire when the page was too large since the _onIFrameLoad code injected by the ScriptManager would execute before the ajax initialisation had time to run.

The first thing I did was consider the interface I wanted to write in each of the aspx pages that required history. In particular I wanted to :

  1. Only have to enter the code to achieve history in one location
  2. Encapsulate as much of the complexities of maintaining browser history as possible.
  3. Allow the plugin to be extendable without over-engineering it!

I decided that keeping it client-side and written in javascript would be the best approach and using my favoured library, jQuery, would simplify integration. And this is the interface I came up with :

<script type="text/javascript" language="javascript">
   $(document).ready(function() {
       $.fn.ajaxHistory.registerCallbacks(getHistory, setHistory, refreshHistory);
       $("#<%=ddlStore.ClientID%>").ajaxHistory();
       $("#<%=btnSearch.ClientID%>").ajaxHistory();
   });
   
   function getHistory() {
       return {
           "store": $("#<%=ddlStore.ClientID%>").val(),
           "surname": $("#<%=txbSurname.ClientID%>").val()
       };
   }

   function setHistory(state) {
       $("#<%=ddlStore.ClientID%>").val(state.store || "0");
       $("#<%=txbSurname.ClientID%>").val(state.surname || "");
   }

   function refreshHistory() {
       $("#<%=btnSearch.ClientID%>").click();
   }
</script>

This code his very basic indeed and doesn’t really include an functionality whatsoever. The ready() function registers three callback with the refreshHistory callback optional. getHistory() is called to return a javascript object containing all the data that must be stored in order to return the page back to a the current state. setHistory() performs the reverse of getHistory(). And refreshHistory() is called upon to refresh the page using the restored state and therefore fetch any updates from a server. In this particular example, the page is a search screen where the user can select a store from a drop down list (ddlStore) which is configured for AutoPostBack and refreshes the search results or they can enter a surname and click the search button (btnSearch.) In the ready() I therefore tell .ajaxHistory(), my plugin, the controls that, when clicked or changed, are required to mark a history point.

The plugin itself is written using a standard pattern and over all looks like this :

(function($) {
    var _elements = new Array();

    $.fn.ajaxHistory = function() {
        return this.each(function() {
            var element = "#" + this.id;
            bindEvent(element);
            _elements.push(element);
        });
    };

    $.fn.ajaxHistory.initialise = function() {
        Sys.Application.add_navigate(onNavigate);
        Sys.WebForms.PageRequestManager.getInstance().add_endRequest(onEndRequest);
    };


    // onEndRequest will be called each time an ajax call is completed and therefore we can rebind all the events.
    function onEndRequest(sender, e) {
        $.each(_elements, function(index, item) {
            bindEvent(item);
        });
    };

})(jQuery);

$(document).ready(function() {
    $.fn.ajaxHistory.initialise();
});

The plugin initialises a couple of Microsoft Ajax events in its own ready() method and the ajaxHistory() method binds the passed elements (specified by a selector) to events that inform the plug in when the element is clicked (buttons, links) or changed (textboxes, drop downs). Since my plugin is used within UpdatePanels the onEndRequest() will be fired at the end of each Ajax call and re-bind the controls (I would use the jQuery .live() binding however binding the change event in IE using jQuery 1.4.2 seems to be broken at the moment and therefore I have to revert to jQuery 1.3.2 instead.)

To remember history points I’m making use of the history functionality built into Microsoft Ajax. This requires that the ScriptManager control is set with the attribute EnableHistory turned on :

<asp:ScriptManager ID="ScriptManager1" runat="server" EnableHistory="true" />

The Microsoft Ajax history functionality is then used in the plugin when the events bound to each of the elements is fired and simply adds a history point calling the getHistory() callback. This code store the state as a query string preceeded with a # which forces the browser to log it in it’s history and can even be bookmarked :

Sys.Application.addHistoryPoint(_getHistory(), document.title);

When the user clicks Back in the browser window, Microsoft Ajax will automatically detect the state after the # and raise the onNavigate event passing the state to it. The plugin simply passes that state to the setHistory() callback and then calls the refresh callback.

Microsoft Ajax Bug

Josh Close has already blogged about the bug I had whilst writing this plugin. My initial testing didn’t have any problems but when I tried to integrate my plugin to a larger page I suddenly found the onNavigate event would not fire and therefore my page would not return to a previous state.

My own debugging in the Microsoft Ajax javascript lead me to the _onIFrameLoad javascript that loads the state and it was Josh’s article that helped me understand that it was running too early. To solve this problem I made sure that the history was initialised as soon as possible rather than last in the page and therefore I made the call to _enableHistoryInScriptManager() myself immediately after the ScriptManager. Thankfully this method doesn’t do much anyway but ensures the write state is set up for the onNavigate event to be fired as expected.

<asp:ScriptManager ID="ScriptManager1" runat="server" EnableHistory="true" />
<script type="text/javascript" language="javascript">
    Sys.Application._enableHistoryInScriptManager();
</script>

Hopefully this problem will go away in ASP.NET 4!

September 21, 2009

ASP.NET Page Life Cycle Diagram

Filed under: ASP.NET, Tip — philiphendry @ 7:42 pm

I saw this months ago and wondered where it had gone so when I came across it by chance whilst googling I thought I would save myself a link to it. Many thanks to Raymond Lewallen who credits Leon Andrianarivony!

image

September 18, 2009

Generic Sorting Routine for ASP.NET GridView

Filed under: ASP.NET, Dev Problem, LINQ — philiphendry @ 7:41 pm

I noticed a lot of code in the current project I’m working on which looked something like this :

switch (sortExpression)
{
    case "Name":
        items.OrderBy(i => i.Name);
        break;
    case "Date":
        items.OrderBy(i => i.Date);
        break;
    case "Cost":
        items.OrderBy(i => i.Cost);
        break;
}

This was then repeated all over again for descending orders!! I’ve now replaced it with something like this :

private void Populate(string sortExpression)
{
   var items = GetData();
   gvList.DataSource = CreateValueList(items).OrderByExpression(new OrderByExpression(sortExpression));
   gvList.DataBind();  
}

The sort expression is handled by the GridView code such that AllowSorting is turned on and the OnSorting event is wired up to a method :

<asp:GridView ID="gvList" runat="server" AllowSorting="True" OnSorting="gvList_Sorting" DataKeyNames="ID">
   <Columns>
       <asp:TemplateField HeaderText="Name" SortExpression="Name">
           <ItemTemplate>
               <asp:Label Text='<%# DataBinder.Eval(Container.DataItem, "Name") %>' runat="server" ID="lblName" />
           </ItemTemplate>
       </asp:TemplateField>
   </Columns>
</asp:GridView>

The code-behind deals with the sorting events and calls populate :

protected void gvList_Sorting(object sender, GridViewSortEventArgs e)
{
   Populate(e.SortExpression + " " + GetSortDirection(e.SortExpression));
}

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

What this all boils down to is the OrderByExpression() extension method called in the Populate() method above:

   public static IEnumerable<TSource> OrderByExpression<TSource>(this IEnumerable<TSource> data, OrderByExpression expression)
   {
       string sortOrderMethod = (expression.SortOrder == SortOrderEnum.Ascending) ? "OrderBy" : "OrderByDescending";

       // data.OrderBy(o => o.propertyname);   
       var dataAsQueryable = data.AsQueryable<TSource>();
       ParameterExpression lambdaParameter = Expression.Parameter(typeof(TSource), "o");
       MemberExpression member = Expression.PropertyOrField(lambdaParameter, expression.SortProperties[0]);
       LambdaExpression lambda = Expression.Lambda(member, lambdaParameter);
       Type[] argumentTypes = { dataAsQueryable.ElementType, lambda.Body.Type };
       MethodCallExpression orderBy = Expression.Call(typeof(Queryable), sortOrderMethod, argumentTypes, dataAsQueryable.Expression, lambda);
       return dataAsQueryable.Provider.CreateQuery<TSource>(orderBy);
   }

I have to say thanks to Joseph Albahari and Ben Albahari of LinqPad fame since I found this solution to my problems (after getting within so close through my own efforts whilst our internet connection was down in the office!!) in the samples that came with LinqPad.

The code above creates a dynamic Linq expression then executes it through the queryables provider – although this only occurs once the whole linq expression is enumerated.

The last few modifications I need to make require the order by to cope with multiple properties (The OrderByExpression.SortProperties class already supports multiple properties but I’m only taking the first at the moment) and I need to ensure that the query always deferred since composable against a database – this is important since I’ve yet to consider paging in the GridView and I want to make sure that specifiying .Skip(n).Take(m) can be applied after the ordering but the Linq expression be composed as SQL and executed against the database in one go otherwise I’ll be returning all the rows to the application layer before extracting just the required rows for the current page.

August 21, 2009

Encapsulating methods of persistence

Filed under: ASP.NET, Code, Design — philiphendry @ 8:09 pm

Here’s a question. What’s wrong with this code :

image

To be honest, there’s nothing wrong with it. And most applications would be absolutely fine. However, I find there are things I don’t like :

  1. The need to type cast.
  2. The use of a string to index the session – if this code is repeated elsewhere only testing will discover whether one of them was typed incorrectly.
  3. The use of the Session object itself!!

That last point is probably my biggest reason for writing the code a differently. When UserAccess is fetched elsewhere in the code, each line will be hardcoded with the method of access. If I changed my mind and wanted to place the object elsewhere, in view state or configuration file for example, then I’d have more places to edit. So what would I replace it with?

image

Seems like a lot of effort but some of that could be reduced by using templates if you don’t want to type too much! I cache the object so no matter how many times I use the property I’m not hitting the Session object and casting.  I’ve also added the property as virtual so a subclass of the page could override the access and replace it with something entirely different – which could be very useful for wrapping the code behind for the page in a unit test and therefore removing any need for ASP.NET objects.

I’ve highlighted this fairly small idea because it highlights some important concepts including that of writing code that maintains the original intent (line 3 now doesn’t have to include any code that indicates where or how the user object is stored) and isolating persistence methods even within a class.

Theme: Shocking Blue Green. Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.