TypeConverter, Arrays and ASP.NET

I've been spending a bunch of time today doing some customization of some ASP.NET custom controls (yes, I know!). One of the things I needed to get working was decent design time support for a property of the control that returned an array of a simple custom structure.

[Browsable(true)]
public MyStruct[] StructData {
   get { return _structData; }
   set { _structData = value; }
}

This should've been simple, but it took me a far longer time than I expected to get working correctly. Getting the basic design time work done wasn't too much of a problem; since the property type was an array and not a custom collection, the default editors did the right thing and offered a good design time experience out of the box.

However, you still need to provide the designers and ASP.NET a way to store the property contents as an string, so that you can use markup to specify the value of the property.

Time to write a TypeConverter! I went ahead and wrote a simple TypeConverter that would take a string and return an array of structs based on its contents, or take an array and return a string based on the values in it. Nothing fancy.

I then enabled the type converter by using the TypeConverterAttribute on the property declaration:

[Browsable(true),
TypeConverter(typeof(MyStructArrayConverter))]
public MyStruct[] StructData {
   get { return _structData; }
   set { _structData = value; }
}

Once I added this, the designer worked right away. I could change the array contents in the designer, and it would get serialized into the aspx page markup correctly. Loading the page worked as well and all values would be correctly deserialized.

I thought I was home free until I tried to actually build the ASP.NET site (this was on a VS WebSite, not a Web Application Project). As soon as the ASP.NET compiler tried to compile the ASPX page hosting the control, it would error out with the dreaded error message:

Page.aspx (web): Object reference not set to an instance of an object.

That's it. No stack trace, no more details at all. I knew it was related to the TypeConverter, but that one was working correctly (already tested and verified). Checking further, I came to the conclusion that the error would appear every time that my TypeConverter.ConvertFrom() implementation would return an array with a non-zero size (useful, huh?).

So the array conversion was working perfectly, but ASP.NET was choking on the values contained in the array. At this point I realized the problem wasn't with the TypeConverter itself, but with how the ASP.NET was manipulating the values returned to generate the code from the ASPX page.

After much head-scratching and finding little explicit documentation about the issue I ran into this documentation piece on MSDN. The last section in the article talks about how "To implement a type converter that produces constructor-based property initialization code".

Based on that, I wrote my yet another custom TypeConverter that, following the guidelines on the article, implemented ConvertTo to convert from a MyStruct value to a corresponding InstanceDescriptor object. I applied this second TypeConverter to the struct declaration itself:"

[Serializable,
TypeConverter(typeof(MyStructConverter))]
public struct MyStruct {
   //...
}

And lo and behold, this time it worked perfectly: No complication errors anymore! Hopefully, next time I run against this and have forgotten again all about TypeConverters and its brethren, I'll remember to look here for hints :-).

Technorati tags: ,

Using Monorail

I've been writing a simple (but important for me) Web Application these past few days using Monorail, and I will just say it totally rocks. Getting started with a monorail project, without using any wizard is actually pretty easy; I started by getting the most recent successful build. I was able to get the basic boilerplate code, even supporting localization, in very little time, and I've been very happy with just how simple and intuitive it is overall.

The only problem I had a bit was with documentation, as it seems to be a little older and doesn't quite match now. For example, the wiki entries for the Brail view engine (which I prefer over the nvelocity one) suggest templates and layouts should be named with a *.boo extension, when it should be *.brail.

Oh, and by the way, writing brail templates in Visual Studio sucks :-)... if you open the files using the HTML editor it works, but it is too helpful in reformatting the code, resulting in broken boo snippets.

Technorati tags: , ,

Janrain.OpenId and .NET 2.0

I've been doing some work this week integrating with OpenID, using Janrain's OpenID library. Scott Hanselman talked about his experience with it a few weeks ago, though I'll admit that frankly, the fact that the library is in Boo didn't really cause me any concerns. I still don't know Boo, really, but the syntax is simple and clear enough that following the code and even attempting a few modifications wasn't really a problem. I do want to take a deeper look at it, though, as it is pretty cool.

But I disgress. This post is not really about Boo, but about actually putting the library to good use to build an OpenID-enabled consumer application. The API for the library is actually pretty straightforward, and takes care of most of the grunt work of implementing a Smart-mode consumer application (as far as I can see the public API doesn't allow you to implement a pure dumb-mode application). However, after fighting a lot with it, I still couldn't get it to work correctly.

Here's what happening: I was pretty quickly able to generate an initial redirect URL for the initial authentication process that would take me to the identity provider's sign on page. In my case, I'm using MyOpenID for testing. That part worked well and I was able to sign in using my openid url. However, the redirect back to my application after signing on was failing: MyOpenID would, at the last step, present an error screen saying that the logon request had an invalid value for the openid.mode. I was using checkid_setup, and I could see that perfectly well in the redirect URL, so surely that wasn't it. Or was it?

After a lot of fiddling around I came to the conclussion that indeed the problem was not the mode argument, but that the redirect URL that Janrain's OpenID library was generating. I started looking at the library code (which is actually very, very readable) and adding a bunch of Debug.WriteLine() statements for tracing.

What I discovered was that the library actually takes great care to ensure that the redirect URLs it creates are wellformed, manually encoding all arguments that are added into the URL QueryString. This is particularly important because with OpenID you send a lot of encoded URLs in the query string.

At last I was able to discover that the URL got mangled in the last step just before it was returned to the application. Well, mangled isn't the right word. What actually was happening was that the arguments in the query string of the generated URL were getting un-escaped, which in turn was causing MyOpenID to choke on them at the end of the authentication process. The last few lines I'm talking about are in the AuthRequest.boo file and are this:

redir = UriBuilder(self.endpoint.ServerUrl)
UriUtil.AppendQueryArgs(redir, qs_args)
UriUtil.AppendQueryArgs(redir, self.extra_args)
return Uri(redir.ToString(), true)

Now, Janrain's library was originally written for .NET 1.1 (although the site claims it supports .NET 2.0 as well), but I was running in .NET 2.0. After looking a bit in the documentation, I ran into the the docs for this specific overload of the System.Uri class constructor, which is now deprecated:

"The constructor has been deprecated. Please use new Uri(string). The dontEscape parameter is deprecated and is always false"

Of course, the orgiinal developers of the library knew very well they had already escaped their querystring properly and didn't want the Uri class to mess with it, but since the Uri class in .NET 2.0 simply ignores the dontEscape parameter and goes ahead and escapes it, it ended up breaking things up pretty heavily. It's not that it unescaped it, it was more that it didn't deal very well with embedded URLs in the query string.

Now, I wanted to see if I could work around it without changing anything in the library, and found the promising Uri.EscapeUriString() method. I figured I could try to run the screwed up Uri I got from the library and see if it fixed it. No dice, it didn't deal with the URLs any better. I have yet to found a good way to work around this issue while keeping the use of  the Uri class here, so if anyone has any suggestions, I'd sure appreciate them.

For now, I resorted to simply modifying the CreateRedirectUrl() method of the AuthRequest class to return the URL as a string instead of a System.Uri instance, like this:

return redir.ToString()

It's a bit of a hack, and I really didn't want to modify the library, but at least for now it allowed me to successfully pass the authentication process. My work is not yet complete, though, as I'm running into a separate issue with session state that has nothing to do with OpenID at all, but I can work around that in time.

If you're using this library, this might be something to keep in mind!

Technorati tags: , , ,

Automatic locale detection on Web applications

Many web sites around will use techniques to try to discover automatically a user's preffered locale when he accesses the site for the first site, in order to provide the user with content in his native language, for example.

There's nothing inherently wrong with this, but I don't personally like it. For example, one such site is the Microsoft Knowledge Base: Go directly to a KB article, and the site will present me with the article in spanish (most of the time a crappy automated translation that's useless, but that's besides the point). I can honestly sympathize with the intention of the site designers, but the end result is extremely annoying to me as a user.

Here's the deal: While I am a native-spanish speaker, and I live in a country with spanish as the official language, I rarely want to get to the spanish version of a site like the MS KB. One of the reasons is simply personal preference, but most significant is the fact that there are many variations of spanish around the world, particularly when it gets to technical jargon. Here we tend to use the english words for a lot of things, even if there are spanish translations for them. Thus, when I get to, say, a spanish KB article, I have to do a lot of effort just to understand what it says because it uses a lot of terms I'm not familiar with. It's ironic, but it actually means it's easier for me to understand the english site than the spanish one!

Because of things like this, I hate sites that try to guess where I am and provide the translated content. I much prefer sites to ask me if I want translated and locale-specific content and from where and then save my prefference, instead of blindingly trying to guess what I want. This is particularly true with poorly implemented sites such as the MS PSS site, which doesn't use consistent rules for presenting translated content (for example, for me right now the main site will show me english text while the KB articles will present in spanish; makes no sense).

When we set out to create our company site, we needed to provide localized content in English and Spanish, and we explicitly decided to go with a different approach: Don't guess, let the user decide. You want the english site? fine. You want the Spanish site? Fine as well!

Instead of saving cookies with your preference, we took advantage of URL rewriting and the localization facilities in ASP.NET 2.0 to provide what we consider a superior alternative: The english site is under the /en/ relative path, while the spanish one is under the /es/ site. Once you've selected the language you wanted, all links consistently keep you in that same language (the power of relative URLs). There's no need to mess with cookies, no need to fuss with query parameters or any other thing like that.

An added benefit of this is that it is very easy to link to a page in a specific language you wanted, since the page URL in the browser clearly establishes which version of the site you're viewing, you can just copy & paste directly!. Bookmarking it will work just as well.

ASP.NET Postbacks and Viewstate Considered Harmful

James Higgs posted an interesting rant here on the Postback and Viewstate mechanism in ASP.NET, saying they can turn an application into a nightmare to maintain.

While I'm not sure I'd agree that I would never use them (I certainly have and probably end up doing it again because I'm a lazy bastard), James makes some really strong points against both mechanisms, and I do tend to agree (in principle at least) with most of them. They certainly reflect on things that bothered me about the ASP.NET execution model since the beginning:

  • The core foundation in ASP.NET is really strong, and way cool: IHttpHandler's, HttpModules and so on are really powerful mechanisms.
  • ASP.NET by default is totally built around post-backs, which are really annoying for navigation. This improved somewhat in v2.0, but the model makes things that were really easy in old ASP (yuck, who would've thought I'd be saying that!) because its model reflected fairly closely the GET/POST paradigm inherent to HTTP, and allowed you good control as to where to use each one. By contrast, ASP.NET pretty much expects you to use POST 99% of the time, with the implications:
    • Bookmarking can be made pretty impossible in some scenarios if you're not careful
    • You can end up with a lot of useless redirects which could've simply being done by taking the user to a different page directly from the browser (again, somewhat improved in v2.0)
    • Back/Forward buttons don't always work as you intended them
    • Link controls that use Javascript and post-backs for navigation. Who the heck came up with such a crappy idea? They also completely screw the navigation experience by making it impossible to open the link in a new tab/window.
  • Viewstate... well, is there anything good to say on it?

The good part about all of this is that ASP.NET continues to improve, and certainly there are a lot of good things in v2.0, and I'm hopeful yet about what the next versions will bring to the table on these fronts.

ASP.NET SiteMenu and custom SiteMapProviders

If you're building an ASP.NET 2.0 site that uses the SiteMenu control with a custom SiteMapProvider implementation as the underlying data source, take care how you populate the properties of the SiteMapNode objects you return from your custom SiteMapProvider class. While trying to fix some code here I noticed that the selected (i.e. current) menu item was not being marked correctly in the generated HTML, and eventually tracked down the problem to using an incorrect value in the SiteMapNode.Key property.

It seems that the MenuItem implementation of IHierachicalData.Path property returns the value of the Key property of the underlying SiteMapNode object it was created from (during databinding), instead of the Url of the node, but during databinding, the SiteMenu control compares the Url of the current page (i.e. the current node in the SiteMapProvider) to the IHierarchicalData.Path implementation of each databound menu item to determine which one is the selected one. If the Key property of the SiteMapNode does not match its Url, then it doesn't work.

Manipulating the SiteMapDataSource for ASP.NET 2.0 Menus

I've been spending some time with ASP.NET 2.0 lately. There are many interesting and very useful new features (Master Pages for example being one of my favourites). Some features however require some more effort to make them work as you'd like (as everything, I guess). Two of those are the Menu and the SiteMapDataSource controls.

Basically, what I was trying to achieve was a very basic top-level, horizontal navigation bar for the site I was working on. The first hurdle was getting the Menu control to generate even remotely useful HTML code, as by default it will generates a mess of nested HTML tables somewhat awkward to style using CSS. Fortunately, I had read ScottGu's entry on the CSS control adapters toolkit, so I downloaded and took a look at the code and was able to use that to instead generate <ul> and <li> HTML tags that were be more workable. I did make a few modifications to the default adapter included in the tooklit to wrap my horizontal menu in <span> tags instead of <div> tags, which allowed me to put some other stuff next to the menu I needed.

With that out of the way, I was running into a little bit of a snag. SiteMaps can only have a single root, which was ok by me and suited the site just fine. The problem was that I wanted the navigation bar to include both the top-level node in the SiteMap (i.e. "Home") as well as all the entries in the second level of the SiteMap (i.e. the real content pages). Having the SiteMapDataSource control driving the menu ignore the root of the SiteMap was not an option, and leaving it as is didn't fit the bill, either.

I could've just used an external XML data source instead, but the SiteMap really worked for my purposes (and was used to drive a SiteMapPath control as well), and well, I'm a sucker for punishement and wanted to check out if I could beat this thing into submition. As it turns out, I could :-)

What I did probably can be called an ugly hack: I created a SiteMapDataSource-derived control that overrides the GetHierarchicalView() method. In it, I grab the SiteMap, clone the root, remove the children and then create a new SiteMapNodeCollection that contains both the cloned root as well as the original second-level items, then return a new SiteMapHierarchicalDataSourceView created from that. Strangely enough it works. Here's what the code looks like (sort of):

public class CustomSiteMapDataSource : SiteMapDataSource
{
   protected override HierarchicalDataSourceView GetHierarchicalView(string viewPath)
   {
      SiteMapNode root = Provider.RootNode;
      SiteMapNode newRoot = root.Clone();
      newRoot.ChildNodes = new SiteMapNodeCollection();
      SiteMapNodeCollection collection = new SiteMapNodeCollection();
      collection.Add(newRoot);
      collection.AddRange(root.ChildNodes);
 
      return new SiteMapHierarchicalDataSourceView(collection);
   }
}

I then just had the Menu control get it's data from a CustomSiteMapDataSource object instead of a normal SiteMapDataSource. Note the code above doesn't have any decent error checking and most certainly will always return just the two top levels of the menu, but it worked for my purposes.