Welcome to Neudesic Blogs Sign in | Join | Help

Compact Framework serial port object hangs up on finalization

It looks like there is a bug in the Compact Framework 2.0 serial port implementation. It actually manifests itself in different ways. In my case it occurred when I tried closing a SQL CE connection, but tracing the problem further revealed the true offender.
 
When serial port is accessed via System.IO.Ports it is touching some unmanaged resources that require finalization. When the garbage collector kicks in, some finalizer hangs up and never returns effectively freezing your process. The reason it happens consistently when closing the SQL CE connection is because the GC is fired off when the last SQL connection is closed. This leads to a misconception of something being wrong with SQL CE.
 
Here is the call stack of the event:
 
 
  mscorlib.dll!System.PInvoke.EE.GC_WaitForPendingFinalizers() 
  mscorlib.dll!System.GC.WaitForPendingFinalizers() + 0x5 bytes 
  System.Data.SqlServerCe.dll!System.Data.SqlServerCe.SqlCeConnection.ObjectLifeTimeTracker.Close() + 0x169 bytes 
  System.Data.SqlServerCe.dll!System.Data.SqlServerCe.SqlCeConnection.Close(bool silent = false) + 0x27 bytes 
  System.Data.SqlServerCe.dll!System.Data.SqlServerCe.SqlCeConnection.Close() + 0x7 bytes 
 
There is very little information about this on the web. I could only find one post with a similar problem -
 
 
This guy is working with the barcode scanner, which ultimately is using the serial port.
 
I solved the problem by switching to a third-party com-port component called SerialNET by Franson (.com). It works like a charm. There are other similar components out there as well.
 

Formatting currency according to the 3-character ISO 4217 code

.NET framework provides us with correct currency formatting based on the CultureInfo.NumberFormat settings. There are two ways to change the display of the currency values:

1. Set CurrentCulture on the Thread:

   Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");

This method is used if you need to change currency formatting for all objects on the page. Don't confuse CurrentCulture with CurrentUICulture. CurrentUICulture is set to the culture of Windows installation, i.e. the language of the OS.

2. Use ToString() method with FormatProvider

   amount.ToString("C", new CultureInfo("en-US").NumberFormat);

NumberFormat member implements IFormatProvider and has properties that define the look of the currency in this particular culture. This method can be used when you need to display amounts in different currency on the same page, e.g. in a multi-currency grid.

The problem

One difficulty that is being discovered by many people is that in order to format currency according to a particular culture, one must know the .NET-centric culture name (en-US) or its numerical LCID to instantiate the CultureInfo object. In reality, currency is often identified by a 3-letter code (ISO 4217) such as USD, GBP, JPY, EUR etc. Suppose you have a table with Amount and Currency columns, such as:

-------------
1234       USD
456342    JPY
7894       USD
-------------

You need to display these values in the grid and use the correct currency formatting for each value. Unforunately the .NET framework provides no mechanism to map the 3-letter code to a culture identifier. When looking for a solution I have seen many variations with lookup tables, where the 3-letter code would map to the culture LCID. Other people resorted to writing their own formatters, which was even less appealing. There had to be a better way!

The solution

There is another class in .NET globalization namespace called RegionInfo. This class has the ISOCurrencySymbol property, which is the 3-letter ISO 4217 code we are after. And it is instantiated the same way as the CultureInfo object via culture name or culture ID. If we can find the RegionInfo via the currency ISO code, we can find the corresponding culture ID and create the CultureInfo object for currency formatting.

There is no method that will return a RegionInfo from a currency ISO code, so we need to enumerate all RegionInfo's in the system to find the right one. Unfortunately there is no easy way to get all RegionInfo's. But there is a way to get all cultures via the CultureInfo.GetCultures() static method. All we need to do now is create the RegionInfo object for each culture and check its ISOCurrencySymbol until we find the one we want. A quick and dirty solution looks like this:

protected CultureInfo CultureInfoFromCurrencyISO( string isoCode )
{
   
CultureInfo[] cultures = CultureInfo.GetCultures(CultureTypes.SpecificCultures);

   foreach (CultureInfo ci in cultures)
   {
      
RegionInfo ri = new RegionInfo(ci.LCID);
      
if (ri.ISOCurrencySymbol == isoCode)
      {
   
      return ci;
      }
   }
   return null;
}

Note that we are interested only in specific cultures (CultureTypes.SpecificCultures parameter). Neutral cultures cannot produce the RegionInfo. An example of a neutral culture is "en", whereas specific cultures are "en-US" and "en-GB". Currency is defined for a specific culture such as USD for en-US and GBP for en-GB.

This method will return a CultureInfo from a currency ISO code, which can be used for currency formatting like this:

amount.ToString("C", CultureInfoFromCurrencyISO(currency).NumberFormat);

It will work, but you don't want to use it in any kind of serious production code. It will create up to 130 RegionInfo objects on each call and if you are using it in a long grid (as intended) it will tax the garbage collector significantly.

Improved solution

In order to improve the quick solution we will simply use the method described above to populate a lookup dictionary once and use it for all subsequent calls. Implementation is attached. It is a singleton class CurrencyHelper with the following static methods:

CultureInfo CultureInfoFromCurrencyISO(string isoCode)
string FormatCurrency(decimal amount, string currencyISO)

First method will return the CultureInfo from an ISO code and the second method will format the decimal amount according to the currency ISO code passed as a second parameter.

The benefit of this solution are:

1. No lookup tables and subsequent maintenance
2. No custom formatting code
3. Pure .NET implementation ensures it will work in future versions
4. Fast lookup via a generic Dictionary class

 

posted by Michael Morozov | 0 Comments
Attachment(s): currency.zip

100% Height in Opera 8.02 or later

Users of the Opera browser may have been unpleasantly surprised after upgrading to version 8.02 or later. Some web-sites suddenly shrunk up and wouldn't fill the screen anymore. These web-sites used elements with an attribute Height=100% to control the layout. The description of this problem appears all over the Internet, sometimes in rather strong words:

http://techpatterns.com/forums/about537.html

It has caused a lot of grief to the developers. Some people claimed they had a solution but it still didn't work for me. There is a way to make it work and the workaround is quite simple. Opera didn't stop supporting 100% height attribute on the tables. It still works. However, it is not 100% of the screen height. It is 100% of the height of the containter element. The outmost container is the page itself and some people were able to fix the problem by setting the Height on the <body> tag to 100%. However in ASP.NET there is also a <form> element, which is usually the outermost HTML element on the page. And Opera has changed the way it treats the size of the <form> element. Since it is not a visual element, it is logical that it doesn't have the Height attribute and doesn't have size. It used to be like that. However it is treated as a visual element with some default height in the new versions of Opera and upcoming version 9.0 available as CTP is no exception. Since it doesn't have the Height attribute, we can use the style attribute to set its height to 100%:

<form id="Form1" method="post" runat="server" style="height:100%">

Now the outermost container on the page will fill the page and the table inside the form will also stretch to 100% of the page.

Crystal Reports menus

In Crystal Reports (I am using version 9), if you right-click on a field embedded into a label, a context menu appears with the following items:

Text Formatting...
Format Text...

Can you guess what they do? :) The former opens up the "Font" dialog, the latter goes to the "Paragraph" dialog. Oh, joy...

posted by Michael Morozov | 0 Comments
Filed Under:

Single Sign-On for everyone

Single Sign-On (SSO) is a hot topic these days. Most clients I worked with have more than one web application running under different versions of .NET framework in different subdomains, or even in different domains and they want to let the user login once and stay logged in when switching to a different web site. Today we will implement SSO and see if we can make it work in different scenarios. We will start with a simple case and gradually build upon it:

  1. SSO for parent and child application in the virtual sub-directory
  2. SSO using different authorization credentials (username mapping)
  3. SSO for two applications in two sub-domains of the same domain
  4. SSO when applications run under different versions of .NET
  5. SSO for two applications in different domains.
  6. SSO for mixed-mode authentication (Forms and Windows)

1. SSO for parent and child application in the virtual sub-directory

Lets assume that we have two .NET applications - Foo and Bar, and Bar is running in a virtual sub-directory of Foo (http://foo.com/bar). Both applications implement Forms authentication. Implementation of Forms authentication requires you to override the Application_AuthenticateRequest, where you perform the authentication and upon successful authentication, call FormsAuthentication.RedirectFromLoginPage, passing in the logged-in user name (or any other piece of information that identifies the user in the system) as a parameter. In ASP.NET the logged-in user status is persisted by storing the cookie on the client computer. When you call RedirectFromLoginPage, a cookie is created which contains an encrypted FormsAuthenticationTicket with the name of the logged-in user . There is a section in web.config that defines how the cookie is created:

<authentication mode="Forms">
   <
forms name=".FooAuth" protection="All" timeout="60" loginUrl="login.aspx" />
</
authentication>

 <authentication mode="Forms">
   <
forms name=".BarAuth" protection="All" timeout="60" loginUrl="login.aspx" />
</
authentication>

The important attributes here are name and protection. If you make them match for both Foo and Bar applications, they will both write and read the same cookie using the same protection level, effectively providing SSO:

<authentication mode="Forms">
   <
forms name=".SSOAuth" protection="All" timeout="60" loginUrl="login.aspx" />
</
authentication>

When protection attribute is set to "All", both encryption and validation (via hash) is applied to the cookie. The default validation and encryption keys are stored in the machine.config file and can be overridden in the application’s web.config file. The default value is this:

<machineKey validationKey="AutoGenerate,IsolateApps" decryptionKey=" AutoGenerate,IsolateApps" validation="SHA1" />

IsolateApps means that a different key will be generated for every application. We can’t have that. In order for the cookie to be encrypted and decrypted with the same key in all applications either remove the IsolateApps option or better yet, add the same concrete key to the web.config of all applications using SSO:

<machineKey validationKey="F9D1A2D3E1D3E2F7B3D9F90FF3965ABDAC304902" decryptionKey="F9D1A2D3E1D3E2F7B3D9F90FF3965ABDAC304902F8D923AC" validation="SHA1" />

If you are authenticating against the same users store, this is all it takes – a few changes to the web.config files.

2. SSO using different authorization credentials (username mapping)

But what if the Foo application authenticates against its own database and the Bar application uses Membership API or some other form of authentication? In this case the automatic cookie that is created on the Foo is not going to be any good for the Bar, since it will contain the user name that makes no sense to the Bar.

To make it work, you will need to create the second authentication cookie especially for the Bar application. You will also need a way to map the Foo user to the Bar user. Lets assume that you have a user "John Doe" logging in to the Foo application and you determined that this user is identified as "johnd" in the Bar application. In the Foo authentication method you will add the following code:

FormsAuthenticationTicket fat = new FormsAuthenticationTicket(1, "johnd", DateTime.Now, DateTime.Now.AddYears(1), true, "");
HttpCookie cookie = new HttpCookie(".BarAuth");
cookie.Value =
FormsAuthentication.Encrypt(fat);
cookie.Expires = fat.Expiration;
HttpContext.Current.Response.Cookies.Add(cookie);

FormsAuthentication.RedirectFromLoginPage("John Doe");

User names are hard-coded for demonstration purposes only. This code snippet creates the FormsAuthenticationTicket for the Bar application and stuffs it with the user name that makes sense in the context of the Bar application. And then it calls RedirectFromLoginPage to create the correct authentication cookie for the Foo application. If you changed the name of forms authentication cookie to be the same for both applications (see our previous example), make sure that they are different now, since we are not using the same cookie for both sites anymore:

<authentication mode="Forms">
   <
forms name=".FooAuth" protection="All" timeout="60" loginUrl="login.aspx" slidingExpiration="true"/>
</
authentication>

 <authentication mode="Forms">
   <
forms name=".BarAuth" protection="All" timeout="60" loginUrl="login.aspx" slidingExpiration="true"/>
</
authentication>

Now when the user is logged in to Foo, he is mapped to the Bar user and the Bar authentication ticket is created along with the Foo authentication ticket. If you want it to work in the reverse direction, add similar code to the Bar application:

FormsAuthenticationTicket fat = new FormsAuthenticationTicket(1, "John Doe", DateTime.Now, DateTime.Now.AddYears(1), true, "");
HttpCookie cookie = new HttpCookie(".FooAuth");
cookie.Value =
FormsAuthentication.Encrypt(fat);
cookie.Expires = fat.Expiration;
HttpContext.Current.Response.Cookies.Add(cookie);

FormsAuthentication.RedirectFromLoginPage("johnd");

Also make sure that you have the <machineKey> element in web.config of both applications with matching validation and encryption keys.

3. SSO for two applications in two sub-domains of the same domain

Now what if Foo and Bar are configured to run under different domains http://foo.com and http://bar.foo.com. The code above will not work because the cookies will be stored in different files and will not be visible to both applications. In order to make it work, we will need to create domain-level cookies that are visible to all sub-domains. We can’t use RedirectFromLoginPage method anymore, since it doesn’t have the flexibility to create a domain-level cookie. So we do it manually:

FormsAuthenticationTicket fat = new FormsAuthenticationTicket(1, "johnd", DateTime.Now, DateTime.Now.AddYears(1), true, "");
HttpCookie cookie = new HttpCookie(".BarAuth");
cookie.Value =
FormsAuthentication.Encrypt(fat);
cookie.Expires = fat.Expiration;
cookie.Domain =
".foo.com";
HttpContext.Current.Response.Cookies.Add(cookie);

FormsAuthenticationTicket fat = new FormsAuthenticationTicket(1, "John Doe", DateTime.Now, DateTime.Now.AddYears(1), true, "");
HttpCookie cookie = new HttpCookie(".FooAuth");
cookie.Value =
FormsAuthentication.Encrypt(fat);
cookie.Expires = fat.Expiration;
cookie.Domain =
".foo.com";
HttpContext.Current.Response.Cookies.Add(cookie);

Note the highlighted lines. By explicitly setting the cookie domain to ".foo.com" we ensure that this cookie will be visible in both http://foo.com and http://bar.foo.com or any other sub-domain. You can also specifically set the Bar authentication cookie domain to "bar.foo.com". It is more secure, since other sub-domains can’t see it now. Also notice that RFC 2109 requires two periods in the cookie domain value, therefore we add a period in the front – ".foo.com"

Again, make sure that you have the same <machineKey> element in web.config of both applications. There is only one exception to this rule and it is explained in the next secion.

4. SSO when applications run under different versions of .NET

It is possible that Foo and Bar applications run under different version of .NET. In this case the above examples will not work. It turns out that ASP.NET 2.0 is using a different encryption method for authorization tickets. In ASP.NET 1.1 it was 3DES, in ASP.NET 2.0 it is AES. Fortunately, a new attribute was introduced in ASP.NET 2.0 for backwards compatibility

<machineKey validationKey="F9D1A2D3E1D3E2F7B3D9F90FF3965ABDAC304902" decryptionKey="F9D1A2D3E1D3E2F7B3D9F90FF3965ABDAC304902F8D923AC" validation="SHA1" decryption="3DES" />

Setting decryption="3DES" will make ASP.NET 2.0 use the old encryption method, making the cookies compatible again. Don’t try to add this attribute to the web.config of the ASP.NET 1.1 application. It will cause an error.

5. SSO for two applications in different domains.

We’ve been quite successful creating shared authentication cookies so far, but what if Foo and Bar are in different domains – http://foo.com and http://bar.com? They cannot possibly share a cookie or create a second cookie for each other. In this case each site will need to create its own cookies, and call the other site to verify if the user is logged in elsewhere. One way to do it is via a series of redirects.

In order to achieve that, we will create a special page (we’ll call it sso.aspx) on both web sites. The purpose of this page is to check if the cookie exists in its domain and return the logged in user name, so that the other application can create a similar cookie in its own domain. This is the sso.aspx from Bar.com:

<%@ Page Language="C#" %>

<script language="C#" runat="server">

void Page_Load()
{
   
// this is our caller, we will need to redirect back to it eventually
   
UriBuilder uri = new UriBuilder(Request.UrlReferrer);

   HttpCookie c = HttpContext.Current.Request.Cookies[".BarAuth"];

   if (c != null && c.HasKeys) // the cookie exists!
   {
      
try
      
{
         
string cookie = HttpContext.Current.Server.UrlDecode(c.Value);
         
FormsAuthenticationTicket fat = FormsAuthentication.Decrypt(cookie);         

         uri.Query = uri.Query + "&ssoauth=" + fat.Name; // add logged-in user name to the query
      
}
      
catch
      
{
      }
   }
   Response.Redirect(uri.ToString());
// redirect back to the caller
}

</script>

This page always redirects back to the caller. If the authentication cookie exists on Bar.com, it is decrypted and the user name is passed back in the query string parameter ssoauth.

On the other end (Foo.com), we need to insert some code into the http request processing pipeline. It can be in Application_BeginRequest event or in a custom HttpHandler or HttpModule. The idea is to intercept all page requests to Foo.com as early as possible to verify if authentication cookie exists:

1. If authentication cookie exists on Foo.com, continue processing the request. User is logged in on Foo.com
2. If authentication cookie doesn’t exist, redirect to Bar.com/sso.aspx.
3. If the current request is the redirect back from Bar.com/sso.aspx, analyse the ssoauth parameter and create an authentication cookie if necessary.

It looks pretty simple, but we have to watch out for infinite loops:

// see if the user is logged in
HttpCookie c = HttpContext.Current.Request.Cookies[".FooAuth"];

if (c != null && c.HasKeys) // the cookie exists!
{
   try
   
{
      string cookie = HttpContext.Current.Server.UrlDecode(c.Value);
      FormsAuthenticationTicket fat = FormsAuthentication.Decrypt(cookie);
      return; // cookie decrypts successfully, continue processing the page
   
}
   
catch
   
{
   
}
}

// the authentication cookie doesn't exist - ask Bar.com if the user is logged in there
UriBuilder uri = new UriBuilder(Request.UrlReferrer);

if (uri.Host != "bar.com" || uri.Path != "/sso.aspx") // prevent infinite loop
{
   Response.Redirect(
http://bar.com/sso.aspx);
}
else
{
   // we are here because the request we are processing is actually a response from bar.com

   if (Request.QueryString["ssoauth"] == null)
   {
      // Bar.com also didn't have the authentication cookie
      
return; // continue normally, this user is not logged-in 
   
} else
   
{

      // user is logged in to Bar.com and we got his name!
      
string userName = (string)Request.QueryString["ssoauth"];
   
      
// let's create a cookie with the same name
      
FormsAuthenticationTicket fat = new FormsAuthenticationTicket(1, userName, DateTime.Now, DateTime.Now.AddYears(1), true, "");
      
HttpCookie cookie = new HttpCookie(".FooAuth");
      cookie.Value =
FormsAuthentication.Encrypt(fat);
      cookie.Expires = fat.Expiration;
      
HttpContext.Current.Response.Cookies.Add(cookie);
   
}
}

The same code should be placed on both sites, just make sure you are using correct cookie names (.FooAuth vs. .BarAuth) on each site. Since the cookie is not actually shared, the applications can have different <machineKey> elements. It is not necessary to synchronize the encryption and validation keys.

Some of us may cringe at the security implications of passing the user name in the query string. A couple of things can be done to protect it. First of all we are checking the referrer and we will not accept the ssoauth parameter from any source other then bar.com/sso.aspx (or foo.com/ssp.aspx). Secondly, the name can easily be encrypted with a shared key. If Foo and Bar are using different authentication mechanisms, additional user information (e.g. e-mail address) can be passed along similarly.

6. SSO for mixed-mode authentication (Forms and Windows)

So far we have dealt with Forms authentication only. But what if we want to authenticate Internet users via Forms authentication first and if it fails, check if the Intranet user is authenticated on the NT domain? In theory we can check the following parameter to see if there is a Windows logon associated with the request:

Request.ServerVariables["LOGON_USER"]

However, unless our site has Anonymous Access disabled, this value is always empty. We can disable Anonymous Access and enable Integrate Windows Authentication for our site in the IIS control panel. Now the LOGON_USER value contains the NT domain name of the logged in Intranet user. But all Internet users get challenged for the Windows login name and password. Not cool. We need to be able to let the Internet users login via Forms authentication and if it fails, check their Windows domain credentials.

One way to solve this problem is to have a special entry page for Intranet users that has Integrate Windows Authentication enabled, validates the domain user, creates a Forms cookie and redirects to the main web site. We can even conceal the fact that Intranet users are hitting a different page by making a Server.Transfer.

There is also an easier solution. It works because of the way IIS handles the authentication process. If anonymous access is enabled for a web site, IIS is passing requests right through to the ASP.NET runtime. It doesn’t attempt to perform any kind of authentication. However, if the request results in an authentication error (401), IIS will attempt an alternative authentication method specified for this site. You need to enable both Anonymous Access and Integrated Windows Authentication and execute the following code if Forms authentication fails:

if (System.Web.HttpContext.Current.Request.ServerVariables["LOGON_USER"] == "") { 
   System.Web.HttpContext.Current.Response.StatusCode = 401; 
   System.Web.HttpContext.Current.Response.End();
}
else
{
   
// Request.ServerVariables["LOGON_USER"] has a valid domain user now!
}

When this code executes, it will check the domain user and will get an empty string initially. It will then terminate the current request and return the authentication error (401) to IIS. This will make the IIS use the alternative authentication mechanism, which in our case is Integrated Windows Authentication. If the user is already logged in to the domain, the request will be repeated, now with the NT domain user information filled-in. If the user is not logged in to the domain, he will be challenged for the Windows name/password up to 3 times. If the user cannot login after the third attempt, he will get the 403 error (access denied).

Conclusion

We have examined various scenarios of Single Sign-On for two ASP.NET applications. It is also quite possible to implement SSO for heterogeneous systems spawning across different platforms. Ideas remain the same, but the implementation may require some creative thinking.

 

ASP.NET 2.0 two-way binding and calculated fields

Did you get that feeling of excitement, when during the ASP.NET 2.0 presentation the two-way bound data grid was built with a few mouse clicks? It does look impressive. However, when you get home and try to do something a little bit more complicated, you hit the wall and end up scratching your head, looking for a magic property that will suddenly make everything work. While Microsoft provides a great number of magic properties and attributes, sometimes you have to resort to actually using the keyboard and writing some code. I particularly like it what a problem can be solved in exactly one line of code. I even wanted to call my blog the "one line fix", but it sounds too much like a stupid cocaine movie title. But the idea remains. I encounter quite a few problems when programming in .NET and if I find a one-line solution for something I will share it with you.

1.

One such problem I encountered when doing the aforementioned two-way data binding. It has to do with having a calculated column. 

It's really quite easy to create a two-way bound data grid:

  1. Drop DetailsView control onto the form
  2. Click on the smart-tag and choose "New data source" in the drop-down list.
  3. Follow the wizard which will build a SELECT statement for you
  4. Click on WHERE button to add a parameter to you SELECT query
  5. Click on ADVANCE button and check "Generate INSERT, UPDATE and DELETE statemens". Your table should have a primary key for this option to be enabled. You also must select the primary key column as one of the retrieved columns.
  6. Finish the wizard and check "Enable editing" in the smart tag panel

Your page source will look something like the following listing. Here is your DetailsView element with 5 bound fields - Id, FirstName, LastName, BirthDate and SunSign (zodiac sign).

<asp:DetailsView ID="DetailsView1" runat="server" AutoGenerateRows="False" DataKeyNames="id" DataSourceID="SqlDataSource1" Height="50px" Width="125px">
   <Fields>
      <asp:BoundField DataField="id" HeaderText="id" InsertVisible="False" ReadOnly="True" SortExpression="id" />
      <asp:BoundField DataField="firstname" HeaderText="firstname" SortExpression="firstname" />
      <asp:BoundField DataField="lastname" HeaderText="lastname" SortExpression="lastname" />
      <asp:BoundField DataField="birthdate" HeaderText="birthdate" SortExpression="birthdate" DataFormatString="{0:d}" />
      <asp:BoundField DataField="sunsign" HeaderText="sunsign" SortExpression="sunsign" />
      <asp:CommandField ShowEditButton="True" />
   </Fields>
</asp:DetailsView>

Here is your SqlDataSource with generated SELECT, UPDATE, INSERT and DELETE commands and parameter lists for each command:

<asp:SqlDataSource ID="SqlDataSource1" runat="server" ConnectionString="<%$ ConnectionStrings:SocionicsConnectionString %>"
DeleteCommand="DELETE FROM [PEOPLE] WHERE [id] = @id"
InsertCommand
="INSERT INTO [PEOPLE] ([birthdate], [sunsign], [firstname], [lastname]) VALUES (@birthdate, @sunsign, @firstname, @lastname)"
SelectCommand="SELECT [id], [birthdate], [sunsign], [firstname], [lastname] FROM [PEOPLE] WHERE ([id] = @id)"
UpdateCommand="UPDATE [PEOPLE] SET [birthdate] = @birthdate, [sunsign] = @sunsign, [firstname] = @firstname, [lastname] = @lastname WHERE [id] = @id">

   <DeleteParameters>
      <asp:Parameter Name="id" Type="Int32" />
   </DeleteParameters>

   <UpdateParameters>
      <asp:Parameter Name="birthdate" Type="DateTime" />
      <asp:Parameter Name="sunsign" Type="Int32" />
      <asp:Parameter Name="firstname" Type="String" />
      <asp:Parameter Name="lastname" Type="String" />
      <asp:Parameter Name="id" Type="Int32" />
   </UpdateParameters>

   <SelectParameters>
      <asp:QueryStringParameter DefaultValue="0" Name="id" QueryStringField="id" Type="Int32" />
   </SelectParameters>

   <InsertParameters>
      <asp:Parameter Name="birthdate" Type="DateTime" />
      <asp:Parameter Name="sunsign" Type="Int32" />
      <asp:Parameter Name="firstname" Type="String" />
      <asp:Parameter Name="lastname" Type="String" />
   </InsertParameters>

</asp:SqlDataSource>

Note that a SelectParameter is a QueryStringParameter. It will automatically pull the "id" from the QueryString and pass it to the SELECT statement. It can also be a ControlParameter pulling the value from another control on the form. It works great for building Master-Detail data forms. The INSERT and UPDATE parameters will be populated from BoundField items in the DetailsView automatically. Very simple.

2.

There is one bug I want to mention. Obviously you would want to format the output for the BirthDate column, since the default representation includes hours, minutes and seconds. Fortunately Microsoft provides a magic property DataFormatString="{0:d}". Unfortunately it doesn't work. Another magic property can be applied to make the data formatting also work in edit and insert mode - ApplyFormatInEditMode="True". This one does work, but in the view mode the value is still shown as a full date-time string. Ahh, screw this magic, let's convert this column to a TemplateField and see if we can format it manually. Open the columns editor, select the "birthdate" column and click on "Convert this field into a TemplateField". You will see that the BoundField expands into this:

<asp:TemplateField HeaderText="birthdate" SortExpression="birthdate">

   <EditItemTemplate
>
      
<asp:TextBox ID="TextBox1" runat="server" Text='<%# Bind("birthdate", "{0:d}") %>'></asp:TextBox
>
   
</EditItemTemplate>

   <InsertItemTemplate>
      
<asp:TextBox ID="TextBox1" runat="server" Text='<%# Bind("birthdate", "{0:d}") %>'></asp:TextBox
>
   
</InsertItemTemplate>

   <ItemTemplate>
      
<asp:Label ID="Label1" runat="server" Text='<%# Bind("birthdate", "{0:d}") %>'></asp:Label
>
   
</ItemTemplate>

</asp:TemplateField>

Note that it created a template for each mode. Edit and Insert templates have a TextBox in them. The view item template has a Label. Also notice the Bind() method call in the server-side tag. This is where the magic happens. Bind() is the new feature in ASP.NET 2.0 which does the two-way data binding. It populates the field with the value from the DataSource and carries the value back into the DataSource parameters. The second parameter is the data formatting string and luckily it works here. The birthdate is formatted now. My guess is that the BoundField implementation simply forgets to pass the DataFormatString into the underlying control. Hopefully it will be fixed in the next version of .NET. But for now we can deal with it by simply converting the column to a template column.

3.

Everything looks good so far, but let's dig a little deeper. Take a look at the "sunsign" column. It is an integer with numbers 1-12 indicating the zodiac sign corresponding to the birthdate. While we can modify this field via the form, it is not the best idea. It would make more sense to calculate this column based on the birth date and update it automatically. Can we do it easily? We can certainly try. Let's convert that field to a TemplateField, and change the TextBox in the Edit template to the Label:

<asp:TemplateField HeaderText="sunsign" SortExpression="sunsign">
   <EditItemTemplate
>
      <asp:Label ID="TextBox2" runat="server" Text='<%# Bind("sunsign") %>'></asp:Label
>
   </EditItemTemplate
>
   <InsertItemTemplate
>
      <asp:Label ID="TextBox2" runat="server" Text='<%# Bind("sunsign") %>'></asp:Label
>
   </InsertItemTemplate
>
   <ItemTemplate
>
      <asp:Label ID="Label2" runat="server" Text='<%# Bind("sunsign") %>'></asp:Label></ItemTemplate
>
   </asp:TemplateField
>
<asp:CommandField ShowEditButton="True" />

Now we have a read-only column. Let's try to change the binding statement. Suppose that we have a CalcSign method that takes a date as a parameter and returns a zodiac sign. Can we do something like this? -

   <EditItemTemplate>
      <asp:Label ID="TextBox2" runat="server" Text='<%# CalcSign(Bind("sunsign")) %>'></asp:Label
>
   </EditItemTemplate
>

Nope we cannot. The Bind() method is not valid in this context. It cannot appear in the parameter list. Fortunately we also have the Eval() method that can:

   <EditItemTemplate>
      <asp:Label ID="TextBox2" runat="server" Text='<%# CalcSign(Eval("sunsign")) %>'></asp:Label
>
   </EditItemTemplate
>

Unforunately, the Eval() method doesn't provide the luxury of two-way binding. While it will work for displaying the correct value, the parameter that is passed into the UPDATE statement will be NULL. So we arrived at the head-scratching post. I still want my two-way binding and I cannot have a calculated column!

4.

The easiest way to solve this problem is to write one line of code. Let's implement an event handler for the ItemUpdating event of the DetailsView. It fires right before the UPDATE statement is executed and in its arguments it has two important collections - NewValues and OldValues. And we can do anything we want with these values:

protected void DetailsView1_ItemUpdating(object sender, DetailsViewUpdateEventArgs e)
{
   e.NewValues["sunsign"] = CalcSign(Convert.ToDateTime(e.NewValues["birthdate"]));
}

In this case we only want to take the new value of the "birthdate" column, convert it to the zodiac sign and replace the value of the "sunsign" column. We should implement the same event handler for ItemInserting event. So here is the one line solution to a common problem.

5.

Another useful event is ItemUpdated (ItemInserted). What we can do here is catch and process the database exception:

protected void DetailsView1_ItemUpdated(object sender, DetailsViewUpdatedEventArgs e)
{
   if (e.Exception != null)
   {
      Debug.Write(e.Exception.Message);
      e.ExceptionHandled = true;
   }
}

This is really useful, because you don't want the yellow screen with errors to appear and since everything is so "code-less", there is no place to implement a try-catch block if anything goes wrong. This try-catch is implemented for you and is wired to the ItemUpdated event. Make sure you set the ExceptionHandled property to true, otherwise the exception will bubble up to the user.

Welcome to the 2-way binding! 

 

 

 

Google tips

Don't you just hate it when you are trying to search for a product review, all you get is middle-man shopping sites? Surely, you add the words "review", "compare" to the product name and model number, but all these sites have a "review" section (with 0 reviews in it) and a "compare" section where they compare prices. In fact, online shopping has become so intrusive, you get a link to a shopping site no matter what you are searching for.

The way I deal with it - I add words to the query that normally don't appear on shopping sites but may be in a review. One such word is "sucks". Google for "xbox 360 sucks" and you will get no shopping sites, only reviews. Very unbiased reviews :) If you want a different kind of reviews, search for word "excited".

I think one feature that Google MUST implement, is a filter for shopping sites. It is not in their best interest, since they get a lot of revenue from these links, but hopefully money isn't everything. If it were, there would have been porn sites poping out on every search - an even better source of revenue. But Google implemented a filter for that. Online shopping sites is the next big threat to humanity. Porn is a lesser threat. It takes balls to support a porn site (pun intended). It takes next to nothing to create and support a shopping site clone. These things are multiplying like rabbits on the wave of the powerful promise of secondary income. Well, somebody's secondary income is becoming a primary concern for everyone else. Uninvited soliciting is what it is!

In fact I'd prefer to see porn links instead of these countless and nameless online store clones. Google, take note! 

posted by Michael Morozov | 0 Comments
Filed Under:

Dependent data-bound controls in ASP.NET 2.0

We all know that in ASP.NET 2.0 we can create data-bound forms without writing any code. You can, for example, create a databound drop-down list using a wizard that takes you through creating a database connection, building the select query and binding it to the control. It will generate something like this:

<asp:DropDownList ID="ddlCountry" runat="server" DataSourceID="SqlDataSourceCountry" DataTextField="country" DataValueField="country" AutoPostBack="True"></asp:DropDownList>
<asp:SqlDataSource ID="SqlDataSourceCountry" runat="server" ConnectionString="<%$ ConnectionStrings:MyConnectionString %>" SelectCommand="SELECT [CountryName] FROM [Country] ORDER BY [CountryName]"></asp:SqlDataSource>

As you can see it created an ad-hoc select statement in the UI. The only thing that is worse than sticking your database queries into the presentation tier is emitting presentation HTML from the stored procedures. Whatever happened to n-tier design, Microsoft? ;) Actually the new code-less features of ASP.NET are great for prototyping. And using it as such, I have encountered an interesting phenomenon, which I want to share with you.

Consider having two drop down lists on the page for Country and City:

<asp:DropDownList ID="ddlCountry" runat="server" DataSourceID="SqlDataSourceCountry" DataTextField="country" DataValueField="country" AutoPostBack="True"></asp:DropDownList>
<asp:SqlDataSource ID="SqlDataSourceCountry" runat="server" ConnectionString="<%$ ConnectionStrings:MyConnectionString %>" SelectCommand="SELECT [CountryName] FROM [Country] ORDER BY [CountryName]"></asp:SqlDataSource>

<asp:DropDownList ID="ddlCity" runat="server" DataSourceID="SqlDataSourceCity" DataTextField="city" DataValueField="city" AutoPostBack="True"></asp:DropDownList>
<asp:SqlDataSource ID="SqlDataSourceCity" runat="server" ConnectionString="<%$ ConnectionStrings:MyConnectionString %>" SelectCommand="SELECT [CityName] FROM [City] ORDER BY [CityName]"></asp:SqlDataSource>

You also want to make these controls interdependent, so when a country is selected, the list of cities is filtered for this country only. To do that you expand the definition of datasource for the City drop-down list:

<asp:SqlDataSource ID="SqlDataSourceCity" runat="server" ConnectionString="<%$ ConnectionStrings:MyConnectionString %>" SelectCommand="SELECT [CityName] FROM [City] WHERE (@VarCountry = 'all' OR [CountryName] = @VarCountry) ORDER BY [CityName]">
   <SelectParameters>
      <asp:ControlParameter ControlID="ddlCountry" DefaultValue="all" Name="VarCountry" PropertyName="SelectedValue" Type="String" />"
   </SelectParameters>
</asp:SqlDataSource>

We have added the ControlParameter to the data source definition, which pulls the selected value from our Country drop-down list and uses it as a parameter to the query. Look ma, no hands! I mean no code. It was all created by the data-binding wizard and this is just one of the many ways to parameterize you queries. This was easy enough. Now when we select a Country, the list of Cities changes accordingly.

Next thing we want to do is add a default value to the list of Countries that will get us all cities unfiltered. You can see that the query above already has an additional clause @VarCountry = 'all' to effectively cancel the filtering by a particular country, when the value of the parameter equals to 'all'. So all we need to do is add this default value to the list of countries as a first element. And ASP.NET 2.0 provides a new way to do just that. You can simply add that value to the list of Items in the designer and set the new property AppendDataBoundItems="True". In the old days, when a control was data-bound, it's existing list of items was cleared. The OnDataBound event was fired later, so we could add our values to the list after it was populated from the data source. Now we can add values to the list in the design mode and they will remain there in addition to the values that come from the data source! How cool is that? Another property that saves you implementing an event handler for the OnDataBound event. But does it, really? 

Let's build and run the application. What happened? Suddenly the filtering doesn't work anymore. The list of cities is always unfiltered regardless of the value in the Country list. The first idea that comes to mind is that the order in which controls are data-bound is changed. Now the City control is databound before the Country control, which initially has only our new "All" element. We remove this element and everything works again. The City control is databound after the Country controls is databound and the ViewState is restored.

If you think about it, it makes certain sense. ASP.NET has to determine the order in which controls are databound based on the dependencies. The City control depends on the value from the Country control, so the Country control should be processed first. But what if this control already has a value? Are we going to wait for it to be data-bound? Apparently not. Having initial values in the list breaks data-binding dependensies! This is not cool.

But crying doesn't really help us get what we want. Coding does! We will forget about the new property AppendDataBoundItems and implement the event handler for the OnDataBound event:

protected void ddlCountry_DataBound(object sender, EventArgs e)
{
      ddlCountry.Items.Insert(0,
new ListItem("All", "all"));
}

This way we get our "All" element into the list after it was databound and the databinding dependencies are still working properly. What about code-less programming? Maybe we will have it in ASP.NET 3.0. Along with the stored procedures that emit client-side HTML.

 

 

Broken page layout in FireFox

A couple of customers reported a problem with the FireFox browser. The page that displayed OK in IE and Netscape would be completely broken in FireFox. It turned out to be an "honest mistake" of the developer.

We all know about the ASP.NET Panel control, which renders itself as a simple <DIV>. It is oftentimes used to hide parts of the page, e.g. specific rows of a form. The form is usually implemented as a table, so we see a construct like this:

<table>
  <asp:Panel runat=server id="myPanel">

  <tr>
    <td>Row 1</td>
  </tr>
  </asp:Panel>

  <tr>
    <td>Row 2</td>
  </tr>
</table>

By setting myPanel.Visible property to false we can effectively hide the table row. While this looks like a legitimate ASP.NET solution, it violates the HTML standards injecting a <DIV> tag in the middle of the table definition. If you only test your pages on IE, you will not notice this problem, since IE is very good at rendering non-compliat stuff. But a few days after your code went into production all those FireFox users will be calling you.

By the way, you can fix this problem rather easily. Just make the <tr> tag itself run at server and hide it just like the Panel: 

<table>
  <tr runat=server id="myPanel">
    <td>Row 1</td>
  </tr>
  <tr>
    <td>Row 2</td>
  </tr>
</table>

But what if you need to hide more than one table row at a time? Well, you have two options:

1. Hide all necessary rows one by one.
2. Catch up with the rest of the world and do table-less layouts with CSS.

Opera displaying images as text

Here is an interesting incompatibility I found with the Opera browser. If you create a custom HttpHandler that takes a URL like http://mysite.com/myhandler.axd and internally does a Server.Transfer to a JPG image, Opera will not recognize it as an image and will display it as text (and I don't mean ASCII-art). It only happens in Opera. Other browsers work just fine.
 
I find it rather amusing, since in theory, Server.Transfer happens on the server before anything is sent to the client. So after Server.Transfer the browser should receive the correct HTTP header along with the image. It should be no different than requesting this image directly through IIS. Apparently there is a difference. It looks like the Server.Transfer doesn't change the content type of the Request, so you have to set it manually before calling the Server.Transfer method:
 
   HttpContext.Current.Response.ContentType = "image/jpeg";
 
Now Opera will not complain!

Welcome

Welcome to my Neudesic blog. I will use it mostly to for programming-related materials, such as interesting issues I come across and solutions to the various problems I encounter in my daily programming routine.