Sunday, March 7, 2010

Control and access the binding ObjectDataSource instance programatically

The declarative data source provides a simple and transparent way of data binding for GridView. However, sometimes we need finer control of the ObjectDataSource instance that a GridView binds to. For example, it's usually easier to generate the footer in the data source rather than doing it in data bound event, like I did in this example. By accessing the ObjectDataSource instance directly, we can control when and how the binding happens as we desire.
In .aspx, we are not specifing the ObjectDataSource in GridView
<ajaxTool:TabPanel runat="server" HeaderText="Transaction Time" ID="serviceTab">
<ContentTemplate>
<asp:GridView ID="gvServiceTime" CssClass="datagrid" runat="server" ShowFooter="True"
          OnRowDataBound="gvServiceTime_RowDataBound" FooterStyle-CssClass="tfoot">
</asp:GridView>
</ContentTemplate>
</ajaxTool:TabPanel>
In .aspx.cs Page_Load, instantiate the DAO object, make the query and then assign it to GridView's DataSource. Then use this DAO object in the OnRowDataBound event to feed the footer. Prefer OnRowDataBound over OnDataBound because I want to customize the data row display and highlight some cells.
public partial class Report : System.Web.UI.Page
{
    ReportDAO _reportDao;

    protected void Page_Load(object sender, EventArgs e)
    {
        ...
        if (reportTabs.ActiveTab == serviceTab)
        {
            _reportDao = _reportDao ?? new ReportDAO();
            gvServiceTime.DataSource = _reportDao.GetServiceTimes(txtStartDate.Text, txtEndDate.Text);
            gvServiceTime.DataBind();
        }
        ...
    }

    /**
     * 1. Format each cell to mm:ss using Util.DurationAsString() rather than raw seconds
     * 2. Generate gvServiceTime footer
     */
    protected void gvServiceTime_RowDataBound(object sender, GridViewRowEventArgs e)
    {
        e.Row.HorizontalAlign = HorizontalAlign.Right;
        if (e.Row.RowType == DataControlRowType.DataRow)
        {
            int max = 0, maxsIndex = 0;
            for (int i = 1; i < e.Row.Cells.Count; i++) // i from 1 to skip the 1st col
            {   
                int duration = (int)_reportDao._report.Rows[e.Row.DataItemIndex][i];
                if (max < duration)
                {
                    max = duration;
                    maxsIndex = i;
                }
                e.Row.Cells[i].Text = Util.DurationAsString(duration);
            }
            if (maxsIndex > 0) // highlight the max cell in the row
                e.Row.Cells[maxsIndex].BackColor = System.Drawing.Color.Beige;
        }
        else if (e.Row.RowType == DataControlRowType.Footer)
        {
            e.Row.Cells[0].Text = "Average";
            for (int i = 1; i < _reportDao._footer.Length + 1; i++)
            {
                e.Row.Cells[i].Text = Util.DurationAsString(_reportDao._footer[i - 1]);
            }
        }
    }

    ...
}
This article also shows another way of adding dynamic columns in a table by using DataTable, which is far more flexible than arrays. Here is some snippet from DAO:
DataTable table = new DataTable();
table.Columns.Add(new DataColumn("TransactionType", typeof(string)));
foreach (string staff in staffs)
{
    table.Columns.Add(new DataColumn(staff, typeof(int)));
}
table.Columns.Add(new DataColumn("Average", typeof(int)));

foreach (TransactionType transactionType in _transactionTypes) // each row
{
    DataRow row = table.NewRow();
    row["TransactionType"] = transactionType.Name;
    int total = 0;
    int n = 0;
    foreach (string staff in staffs) // each col
    {
        ServiceTime serviceTime = counts[transactionType.Name]
            .Where(r => r.Staff == staff).SingleOrDefault();
        row[staff] = serviceTime == null ? 0 : serviceTime.Count;
        total += serviceTime == null ? 0 : serviceTime.Count;
        n += serviceTime == null ? 0 : 1;
    }
    row["Average"] = (int)(total / (n == 0 ? 1 : n)); // last col is average
    table.Rows.Add(row);
}

Thursday, March 4, 2010

Dynamic column in GridView binding an array

Recently I've done some manual reporting using ASP.NET. Well, orginally I developed it easily using SQL Server 2008 Reporting Service, which is not in our production environment yet, but the client was keen to have the application deployed.
Dynamic columns is actually easy in GridView because you can bind the GridView to whatever data source with just a little more work than the simple declarative binding. For example, you can bind it to a DataTable like this. In my case, the report has fixed number of rows each of which represents a transaction type, while the columns are dynamic depending on another database table for zones. To feed this report grid, an OjbectDataSource is created which returns an array of ReportRow.
public class ReportRow
{
    public string CallType { get; set; }
    public int[] Counts { get; set; } // Counts array for each location + the last is total
}
In .aspx file, we must set AutoGenerateColumns to false. The gvImportCounts binds to an ObjectDataSource with 2 parameters for the reporting period. The OnDataBound event is to add a footer in the report.
<ajaxTool:TabPanel runat="server" ID="importTab">
  <HeaderTemplate>Import Counts</HeaderTemplate>
  <ContentTemplate>
    <asp:GridView ID="gvImportCounts" CssClass="datagrid" runat="server" AutoGenerateColumns="False"
         DataSourceID="odsImportCounts" ShowFooter="True" Width="658px"
         OnDataBound="gvImportCounts_DataBound">
      <FooterStyle CssClass="tfoot" />
    </asp:GridView>

    <asp:ObjectDataSource ID="odsImportCounts" runat="server" SelectMethod="GetImportCounts"
        TypeName="DAO.ReportDAO">
      <SelectParameters>
        <asp:ControlParameter ControlID="txtStartDate" Name="start" PropertyName="Text" />
        <asp:ControlParameter ControlID="txtEndDate" Name="end" PropertyName="Text" />
      </SelectParameters>
    </asp:ObjectDataSource>
  </ContentTemplate>
</ajaxTool:TabPanel>
The difficulty of binding to array elements, like Frdrik pointed out in his blog (Ref 1), is that the ASP.NET's standard BoundField does not provide this functionality. So one of the elegant ways is to extend it by ourselves.
In aspx.cs, add this subclass that enables us to evaluate array elements:
/**
* Extend the BoundField to bind column to composite field even array element
*/
public class CompositeBoundField : BoundField
{
    protected override object GetValue(Control controlContainer)
    {
        object item = DataBinder.GetDataItem(controlContainer);
        return DataBinder.Eval(item, this.DataField);
    }
}
The rest important code in aspx.cs:
protected void Page_Load(object sender, EventArgs e)
{
    if (!Page.IsPostBack) // first load
    {   // Dynamically generate columns for Import report:
        BoundField bf = new BoundField();
        bf.DataField = "Import";
        bf.HeaderText = "Transaction Type";
        gvImportCounts.Columns.Add(bf);

        var zones = ReportDAO._zones; // get zones from DAO

        for (int i = 0; i &lt; zones.Count; i++)
        {
            bf = new CompositeBoundField();
            //Initalize the DataField and HeaderText field value:
            bf.DataField = "Counts[" + i + "]"; // bind to array element
            bf.HeaderText = zones[i].Name;

            //Add the newly created bound field to the GridView:
            gvImportCounts.Columns.Add(bf);
        }
        // Add the Total column
        bf = new CompositeBoundField();
        bf.DataField = "Counts[" + zones.Count + "]";
        bf.HeaderText = "Total";
        gvImportCounts.Columns.Add(bf);
    }
}

/**
* Generate gvImportCounts footer
*/
protected void gvImportCounts_DataBound(object sender, EventArgs e)
{
    GridView grid = (GridView)sender;
    GridViewRow footer = grid.FooterRow;
    if (footer != null) // footer defined, so fill it
    {
        footer.Cells[0].Text = "Total";
        for (int i = 1; i &lt; grid.Columns.Count; i++)
        {
            int total = 0;
            foreach (GridViewRow row in grid.Rows)
            {
                if (row.RowType == DataControlRowType.DataRow)
                    total += int.Parse(row.Cells[i].Text);
            }
            footer.Cells[i].Text = total.ToString();
        }
    }
}
Here is the result: Ref:
  1. Fixing BoundField Support for Composite Objects
  2. GridView and dynamic data sources

Wednesday, February 17, 2010

Deletes files older than a number of days in a specified directory

It's not quite easy to achieve that functionality with Windows batch file. However with built-in command wscript.exe or cscript.exe, we can write some jscript:
/**
* Deletes files older than a number of days in a specified directory
*/

function deleteFilesInFolder(folder) {
  WScript.echo(folder);
  for (var it = new Enumerator(folder.Files); !it.atEnd(); it.moveNext()) {
    var file = it.item();
    if (file.DateLastModified < nDaysAgo) {
      if (testing)
        WScript.echo("  " + file.name);
      else
        file.Delete(true);
    }
  }

  if (recursive)
    for (var it = new Enumerator(folder.SubFolders); !it.atEnd(); it.moveNext()) {
      deleteFilesInFolder(it.item());
    }
}

var usage = "Usage: cscript|wscript delOldFiles.js [-t] [-r] [-h|-?] [dir] [#Days]\n" +
    "-h|-?: This usage message\n" +
    "   -t: Test mode, no real deletion, just show files to be deleted\n" +
    "   -r: Recursive, file in subdirectories will be deleted as well\n" +
    "  dir: The specified directory. Default is current dir\n" +
    "#Days: Delete files older than this # of days. Default is 7";

// arguments handling
var testing = false;
var recursive = false;
var dir;
var nDays;

for (i = 0; i < WScript.arguments.length; i++) {
  arg = WScript.arguments(i);

  if (arg == "-h" || arg == "-?") {
    WScript.echo(usage);
    WScript.quit();
  }
  else if (arg == "-t")
    testing = true;
  else if (arg == "-r")
    recursive = true;
  else if (isNaN(parseInt(arg)))
    dir = arg;
  else 
    nDays = arg;
}
nDays = nDays == null ? 7 : nDays;
dir = dir == null ? "." : dir;
//WScript.echo("-t=" + testing + ", -r=" + recursive + ", dir=" + dir + ", day=" + nDays);

// Compute date
var nDaysAgo = new Date();
nDaysAgo.setDate(nDaysAgo.getDate() - nDays);

// Delete files in the dir
deleteFilesInFolder(WScript.CreateObject("Scripting.FileSystemObject").GetFolder(dir));
Ref: Vincent Robert's example

Thursday, December 17, 2009

Sculpture generated Silverlight on IIS and Async_Exception

Sculpture is a .NET open source MDD (Model-Driven Development) code generation framework. With the same model, it can generate various UIs such as Silverlight, ASP.NET MVC and WPF for you. This article talks about tricks of Silverlight, not necessarily Sculpture related, but if people encounter the same problem that I found using Sculpture, they might found it useful.

It is quite easy to run the Silverlight application from within Visual Studio 2008, being it created by Sculpture or yourself. However, to host that application on IIS 6, there are some configuration work to do. Here are the brief steps:

1. Start->Run->inetmgr
2. Right click Default Web Site->Properties->Home Directory->Configuration->find .aspx->Edit->Executable, change to C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll
3. Under Internet Information Services, right click the Server node->Properties->MIME Types->New, add:
Extension: .xap, MIME type: application/x-silverlight
Extension: .xaml, MIME type: application/xaml+xml
4. Restart IIS service


For details, see reference 1.

Now your Silverlight baby should be beautifully running and it's time for dealing with the unhandled exceptions if you use WCF in it. With normal silverlight installed, the typical error is like:
Message: Unhandled Error in Silverlight Application [Async_ExceptionOccurred]
Arguments:
Debugging resource strings are unavailable. Often the key and arguments provide sufficient information to diagnose the problem. See http://go.microsoft.com/fwlink/?linkid=106663&Version=3.0.40818.0&File=System.dll&Key=Async_ExceptionOccurred   at System.ComponentModel.AsyncCompletedEventArgs.RaiseExceptionIfNecessary()
at App.Infrastructure.ServiceContracts.DatabaseServerToExcludeCrudInfoCompletedEventArgs.get_Result()
at App.Shell.ServiceReferences.DatabaseServerToExcludeCrudService.DatabaseServerToExcludeCrudService_GetAllCompleted(Object sender, DatabaseServerToExcludeCrudInfoCompletedEventArgs e)
at App.Shell.ServiceReferences.DatabaseServerToExcludeCrudService.OnGetAllCompleted(Object state)
at System.Threading._ThreadPoolWaitCallback.WaitCallback_Context(Object state)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading._ThreadPoolWaitCallback.PerformWaitCallbackInternal(_ThreadPoolWaitCallback tpWaitCallBack)
at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback(Object state)
Line: 1
Char: 1
Code: 0
URI: http://192.1.1.50/App/Silverlight.js
With silverlight_developer version installed, you could see a bit more:
Message: Unhandled Error in Silverlight Application An exception occurred during the operation, making the result invalid.  Check InnerException for exception details.   at System.ComponentModel.AsyncCompletedEventArgs.RaiseExceptionIfNecessary()
at App.Infrastructure.ServiceContracts.DatabaseServerToExcludeCrudInfoCompletedEventArgs.get_Result()
at App.Shell.ServiceReferences.DatabaseServerToExcludeCrudService.DatabaseServerToExcludeCrudService_GetAllCompleted(Object sender, DatabaseServerToExcludeCrudInfoCompletedEventArgs e)
at App.Shell.ServiceReferences.DatabaseServerToExcludeCrudService.OnGetAllCompleted(Object state)
at System.Threading._ThreadPoolWaitCallback.WaitCallback_Context(Object state)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)...

There are at least 2 things you might have to check/fix:

1. Do you have a clientaccesspolicy.xml or crossdomain.xml under the root of your application domain where the service is hosted? Either file will set rules for your Silverlight control to access a WCF/web service in another domain.

An example of allowing any client is like this:
<?xml version="1.0" encoding="utf-8"?>
<access-policy>
  <cross-domain-access>
    <policy>
      <allow-from http-request-headers="*">
        <domain uri="*"/>
      </allow-from>
      <grant-to>
        <resource path="/" include-subpaths="true"/>
      </grant-to>
    </policy>
  </cross-domain-access>
</access-policy>
For more details of cross-domain access, see ref 3.

2. Have you set the Endpoint Address of WCF service in Silverlight client correctly?

There are several ways of doing that. For example, generate/edit ServiceReferences.ClientConfig. From Visual Studio, you can right click your service node's Service References->Add Service Reference... to generate the client configuration.

For Sculpture generated code, you can override the GetEndPointAddress() method in ServiceConfiguration.cs and set the correct URL.

There seemed to be no big difference by configuration or programming as finally they will be compiled into Silverlight binary and neither has the flexibility of modification on the fly.

Ref:
1. Developing and deploying a Visual WebGui Silverlight application
2. Configuring IIS for Silverlight Applications
3. Using Silverlight 2.0 clientaccesspolicy.xml vs. crossdomain.xml for Web-Service cross-domain access
4. Network Security Access Restrictions in Silverlight
5. Making a Service Available Across Domain Boundaries

Monday, November 23, 2009

Install Silverlight 3 Tools Offline

It seems quite stupid that the Silverlight 3 Tools installation will always look to download from internet even you have installed the Silverlight SDK by yourself. More absurdly, the download and installation will fail even you have connection to internet but by using a proxy, which is common in an office. The trick here is to extract them manually into the same folder:
  1. In cmd window, uncompress Silverlight SDK: silverlight_sdk.exe /x
  2. Uncompress Silverlight tools to the above chosen directory: Silverlight3_Tools.exe /x
  3. In the extracted folder, run VS_SilverlightTools_Setup.exe
  4. In the same folder, run VS90SP1-KB967143-enu.msp as well as VS90SP1-KB967144-enu.msp (ignore this failure)
The last 2 steps take a while, but eventually you should be able to open silverlight projects in Visual studio then.

Ref: Installing SilverLight 3 Visual Studio Tools Offline

Sunday, November 8, 2009

Simple way to invalidate SqlDataSource cache programmatically

EnableCaching and CacheDuration are the normal cache-related properties of the data source controls, but they don't give you the convenience to invalidate the cache on-demand. Some people use the SqlCacheDependency, where ASP.NET worker process will poll for changes in the SqlCacheTablesForChangeNotification when a change in monitored table triggers the "notification". This measure is somewhat clumsy in that:
  1. You have to use aspnet_regsql command line to enable notifications for the database;
  2. ASP.NET uses a polling mechanism, and you have to both write code and alter your web.config
There is a simpler way though: use CacheKeyDependency, where you make the data source cache dependent on another item in the data cache (CacheKeyDependency). Details follow:
  • Add CacheKeyDependency in aspx:
<asp:SqlDataSource ID="x" EnableCaching="True" CacheKeyDependency="MyCacheDependency" />
  • Add some code in aspx.cs:
protected void Page_Load(object sender, EventArgs e)
{ // Or somewhere else before the DataBind() takes place
  if (!IsPostBack)
  {
      ...
      if (Cache["MyCacheDependency"] == null)
      {
        Cache["MyCacheDependency"] = DateTime.Now;
      }
  }
}
  • Where you make changes to the database table and want to invalidate cache so that next time a data binding will see the changes:
// Evict cache items with an update in dependent cache:
Cache["MyCacheDependency"] = DateTime.Now;
You can use any value instead of DateTime.Now as long as they are different to trigger the refresh.
Ref: 1. ASP.NET Caching: SQL Cache Dependency With SQL Server 2000
2. Accessing and Updating Data in ASP.NET 2.0: Declaratively Caching Data

Sunday, November 1, 2009

Prevent IIS Session Timeout in ASP .NET

There are scenarios that a user may want to keep a long session alive. For example, a help desk operator logs into a web application and takes phone calls and in between submits changes to the backend systems. The phone call may last over an hour and the operator may stay in one web page and need that session to be valid when she submits the changes.
In ASP.NET, there are several common simple solutions for that. One of them is to set the session timeout attribute (minutes) in web.config.
<sessionState mode ="InProc" timeout="xxx"/>
Some people are confused at the timeout setting in web.config and another in IIS and ask which overwrites which. A simple experiment shows that the setting in web.config always overwrites the setting in IIS.
However, this is not complete for ASP .NET 2.0+. Open IIS->Application Pools->Select the Application pool for your application->Properties->Performance, set the idle timeout for "Shutdown worker processes after being idle for xxx minutes".
Otherwise, the worker process is still stopped after 20 minutes(default) and your session state will be lost.
Another way, if we do not want to rely on the settings in IIS, is to keep the session alive by ourselves in the application. Some people use an invisible frame and set auto refresh in http header, but if you are using a master page, that behavior will be propagated to all ASP pages. With .NET 2.0+ and a little bit Ajax, we have a simpler solution - add a timer in UpdatePanel. The timer will trigger a partial postback periodically, thus prods the session to be alive.
<asp:Content ID="ct" ContentPlaceHolderID="cphMain" runat="Server">
  <asp:ScriptManager ID="scriptMgr" runat="server" />

  <%-- Your content here --%>

  <%-- Heartbeat every 15min to keep session alive --%>
  <asp:UpdatePanel ID="updatePanel" runat="server">
    <ContentTemplate>
      <asp:Timer ID="timerPing" runat="server" Interval="900000">
      </asp:Timer>
    </ContentTemplate>
  </asp:UpdatePanel>
</asp:Content>
Ref:
1. Managing Session TimeOut using Web.Config
2. Preventing Session Timeouts in C# ASP .NET