Friday, March 02, 2007

Mind over Microsoft: curing DataSet autism

The deeper I dig into Visual Studio 2005 the more I get the impression that it contains more obstacles than features. It isn't enough to just think something over and figure out how to do it: each step of the implementation may hide a showstopper - either a bug or a crippled feature. You have to check very carefuly if all you thought of can really be implemented.

Here's a good example on how you can get sucked into "Visual Studio does everything by itself" Microsoft agitprop. I wanted to work out a simple solution for a non-creative task which is making search forms for my windows application. You know, search: enter what you want to search for and you get a list of records from the database. A form with a data grid in the center, some text and combo boxes above it for the user to enter search criteria into, and a SEARCH button.

What would be the best way to implement this kind of component, to make mass production as fast as possible? Let's see... I'd have to utilise the design-time functionality to the maximum. There could be a base class containing most of the logic and all common components: say, an empty DataGridView which the user could configure from inside a derived component to add columns etc. The dataGrid would be bound to a BindingSource which is in the base class, but its DataSource type (the DataSet used for search operation) should be set from the derived component so the designer could know what properties exist in it.

Then, in the derived component, a developer would add a new data source for the BindingSource - i.e. create a DataSet, modify its sql query and voila, the grid's columns are automatically initialized and prepared for customization. The base class would pick up the DataSet type being used and wire up the logic (i.e. SearchButton_Click handler) for filling the DataSet - after all, the DataSet has a generated TableAdapter configured and ready to go.

Now, how to enter search criteria? This one could be interesting: write the DataSet SQL with prepared parameters in the WHERE part, like "SELECT something FROM something WHERE (column1 = @param1 OR @param1 IS NULL) AND (column2 etc.)". Next, create a separate BindingSource in the base class for search criteria. In the derived class, bind it to the table adapter's parameters, then add text and combo boxes to your liking and bind them to individual parameters. Ok, this could be problematic, but an ICustomTypeDescriptor wrapper class or something could help turn the adapter parameters into something bindable. So, using high-tech Visual Studio designer components, the user gets to make a complete search form in half an hour. Wow!

Um, I hope you didn't read this post from the middle? Because you could have gotten the wrong impression that this could be done. The cruel reality is that most of the abovementioned doesn't work. Let's start from the beginning.

Have you ever looked at the generated DataSet source in VS 2005? I don't know if it's the same as in VS 2003, but it's not just ugly, it's absolutely hideous. I thought that if I notify my component of the type of DataSet being used, it could somehow detect the proper DataAdapter from it. Not likely: not only there is no property on the DataSet that could point to the DataAdapter type (let's say this is ok, there could be more than one DataAdapter for a given DataSet), but the DataAdapter is not even in the same namespace! Why isn't it a class embedded in the DataSet? Nobody thought of that. It doesn't seem there was a lot of thinking invested here anyway.

But this is not the end of it: the generated TableAdapter is not a table adapter at all, it's a class derived directly from Component - in other words, a nobody! The real DataAdapter is in its private property. (Aarrggh, ok, we'll use reflection to access it then). Yeah, but the real adapter is not initialized when the quasi-TableAdapter is instantiated. It's initialized only when you call the Fill method... And since there is no base Fill method (remember, the base class is a Component), the only option is to call the generated Fill method and feed it parameter values. So, if I'm going to use reflection anyway to get the private property, why not call the Fill method directly.

Ok, never mind: use reflection to get the adapter, call all the proper initialization methods using reflection and hope their names don't change in future versions. (Fat chance, I'd bet this code survived from the VB days. I tried finding the DataSet designers using Reflector and stopped when at some point the .Net code jumped through the interop mirror over into the COM world... Well, that's true at least for the sql designer - which is the most useful part of it anyway).

Now, to make an ICustomTypeDescriptor to bind to the sql query parameters. Well, it can't be done: BindingSource doesn't support ICustomTypeDescriptor. Just think of it: what do you get when you add two cool technologies together? Nothing, because they've been made by Microsoft. Do the guys over there talk to each other at all?

So, I resorted to a brute-force solution: created a new wrapper class complete with a VS.Net designer that detects parameters from a given TableAdapter and creates an accessor property for each of them. So I can bind to the wrapper and the wrapper will then call the TableAdapter.

Have you started wondering why I haven't chosen to abandon DataSets and create my independent components? I did think about it - but at this point it is pure bloody-mindedness that drives a man to beat the damn technology. It's a challenge.

Yeah, right. Next step: the DataGrid. In the derived class, we configure the BindingSource to use the generated DataSet and then get the DataGrid to automatically configure its columns from it. Then we just reposition and format the columns and -

Well, surprise surprise: visual inheritance is turned off in VS 2005. (I know it's old news, this is a story that was a long time in the making: just look at the sheer volume of text above). So - man, do you still want to beat the technology?

I had to make my own templating mechanism (an extender provider, actually: these are very useful) so one can put the controls into the derived class and then set their "extender-ed" property that says into which area of the template it should be placed. So the base class picks up thusly marked controls and knows which is which.

Next? Binding issues: the pre-SP1 data binding designers in VS 2005 are extremely fiddly (the post-SP1 ones are just fiddly). You can bind controls to datasources but you aren't sure you'll be able to open the designer again in the future. The class properties don't always appear in the data sources toolbox. And the ones that do, don't appear when you try to bind from the properties window. If you want to bind to a property's property, you'll have to write the path manually. Jesus Christ! Today they'd give you Visual Studio .Net instead of a cross.

Wait, there's more: the DataSet designer resets most of the manually set properties (like parameter or column types or their AllowDbNull property) every time you reconfigure it. Before you start modifying a DataSet, you have to memorize its properties' values first. If you forget a single detail, you either won't be able to compile or you'll introduce a bug you'll waste some more time fixing (possibly at a later time when you forget what you did).

And more: I'm not quite sure how connection strings are supposed to be handled in VS 2005 SP2. If I try adding a new connection string to the project, it doesn't show up in the dataset designer. I suppose the designer caches existing connection strings in the XSD files but never seems to refresh them. If I try to rename one of the connection strings, I either get a refactoring error (the refactoring engine somehow destroys a property in the dataset's Designer.cs file) or I end up with crippled XSD files (half of its XML simply goes missing). Sometimes, for reasons unknown to me, a one of connection string names gets a full path with a namespace, and XSD starts reporting errors until you fix it (turns out it cannot stand dots in connection string names).

I'd already lost enough time battling with Visual Studio 2005 that any productivity improvements from using it - and after all this I seriously doubt there will be any surprises any time in the future - cannot compensate for it. It did sound like a good solution before I started implementing it, but that is because it was based on wishful thinking fueled by Microsoft marketing. One has to be very careful about these things: for any such adventure, multiply your time estimate by three.