Showing posts with label WinForms. Show all posts
Showing posts with label WinForms. Show all posts

Friday, February 24, 2006

DataGridView - SelectionChanged escapade

We use the new .NET 2.0 DataGridView extensively in my current project, in combination with object bindingsources. We have several user-controls where the grid is the position/current source for other controls in the UC, e.g. master-details grids, selected grid-row edit form, etc. All of our grids are in single select/FullRowSelect mode. Thus, the when the selected row changes, so do all the controls bound to the grid (actually bound to the same bindingsource as the grid).

The SelectionChanged event of the DataGridView tells you when the selected row changes, and this event trigger quite often, in fact more often than you would expect. It triggers during:

  • binding: the first row always gets selected
  • unloading: the selections are cleared
  • resize: the selections are first cleared, then all of them gets selected again
  • sorting: the selections are first cleared, then the grid selects whatever rows got the same row index in the grid, which most likely are not the same records that were selected before sorting
Typically, a user-control gets instantiated and initialized by its parent, and then the parent does .Dock=Fill on the UC. Thus, the the SelectionChanged event will trigger three times during load: when a user-controls gets loaded, the DataGridView gets bound and the first row gets selected (first), then the resize event causes unselecting the row (second) and then the reselection og the first row (third).

We do layz loading of the details data, i.e. we do not access the application services to get the detail data until the user selects a row in the grid. I noted that the UC took a long time to load, and when I added a breakpoint to see why, I was quite surprised when the data got loaded three times during load. The extra load was caused by me setting a selected row after binding, as this had to be done in .NET 1.1 grids from Microsoft, but is no longer needed as the DataGridView always select the first row. I wish there was an option on the grid to turn the auto-select off.

Thus, what is needed is some extra logic to remember which record is the current in the binding source (note: not which row in the grid), then check in the SelectionChanged event whether the current record actually was changed, or whether it is just the eager event triggering. Use a helper class member to keep the ID of the selected record. Do your data loading only when the current record changes, otherwise, just rebind to the already fetched data.

The next (expected) surprise I got was when I added sorting to the binding source of the DataGridView. The same effect happended as during the load resizing: the selections got cleared and then set again, but it does not select the same records again, it only selects the same rows as before. As the records are now sorted a different way, the record previously at row 4 is now e.g. at row 17, but the grid "dutifully" selects row 4 anyway. Thus, you need to implement some find logic, or just call .ClearSelection() on the grid.

I chose to clear the selection after sorting, and wanted to suppress the reselect SelectionChanged event that happens during sorting, as this caused unnecessary binding to random record which I would immediately clear. The grid has a Sorted event that happens when the sorting has completed and after the selection events, but there is no begin sorting event. I needed another helper class member to tell the SelectionChanged event hander that the grid is currently sorting. I thought the ColumnHeaderMouseClick event would let med set an _isSorting boolean flag, but this event happens after the Sorted event, far to late for my use.

I ended up using the MouseDown event and some classical x,y hit testing to deduce that a sort operation was about to begin:

Private Sub dgwMaster_MouseDown(ByVal sender As System.Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles dgwMaster.MouseDown

Dim hti As DataGridView.HitTestInfo = dgwMaster.HitTest(e.X, e.Y)
If hti.Type = DataGridViewHitTestType.ColumnHeader Then
_isSorting = True
End If

End Sub


Private Sub dgwMaster_Sorted(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles dgwMaster.Sorted

_isSorting = False

End Sub

The final surprise when it comes to binding is about the impedance difference between object binding sources and the DataGridView: a DataGridView can have no selection (just CTRL+click a selected row), while an object bindingsource always have a .Current item when the source is not empty. Setting .Postion to -1 on a bindingsource has no other effect that selecting the first item in the binding source. Thus, to allow for no selection in bound controls, you have to remove the .DataSource of those controls temporarily when the grid have no selection.
I wish there was an option to enforce the grid to always have a selection. You can make a dervied grid and override MouseDown and then not call the base class OnMouseDown method to cancel the mouse click.

Due to this impedance difference, always remember to check that a row is selected in the grid before using the bindingsource .Current property to get the currently selected record:

If _isSorting = True Then Exit Sub


'ensure that a row is selected

If dgwMaster.SelectedRows.Count = 0 Then

dgwDetails.DataSource = Nothing

Else

Dim response As OrderDetailsResponse

'get details

Dim master As Order = CType(OrderBindingSource.Current, Order)

If master.orderId = _lastOrderId Then

'avoid reget on resize, just reconnect bindingsource

dgwDetails.DataSource = OrderOverføringBindingSource

Else

_lastOrderId = master.orderId

response = _orderMgr.GetOrderDetails(master.orderId)

_details = response.ItemList

If dgwDetails.DataSource Is Nothing Then

'sorting changes current master, need to reconnect bindingsource

dgwDetails.DataSource = OrderDetailsBindingSource

End If

End If

End If


No wonder that the DataGridView program manager Mark Rideout is quite busy answering questions at the Windows Form DataBinding forum.

Thursday, February 02, 2006

VS2005 inherited user control, abstract factory/dependency injection pattern

Yesterday the need arised for inheriting a WinForms user control to implement four specializations of a user control. Creating inherited user controls is very simple in Visual Studio, just click 'Add-New item' and select 'Inherited user control', name the new user control appropiately and click 'Add'. I then selected the user control to derive from (note that the class must be plain public, while the constructor can be protected) and clicked OK.

VS2005 dutifully added the new user control files, but the VS designer failed with this error message:


One or more errors encountered while loading the designer. The errors are listed below. Some errors can be fixed by rebuilding your project, while others may require code changes.

Object reference not set to an instance of an object.

at System.Reflection.Assembly.nLoad(AssemblyName fileName, String codeBase, Evidence assemblySecurity, Assembly locationHint, StackCrawlMark& stackMark, Boolean throwOnFileNotFound, Boolean forIntrospection)
. . .
at Microsoft.VisualStudio.Design.Serialization.CodeDom. VSCodeDomDesignerLoader.DeferredLoadHandler.Microsoft.VisualStudio. TextManager.Interop.IVsTextBufferDataEvents.OnLoadCompleted(Int32 fReload)


Some googling did provided me with only a few similar problems, but none of the suggested solutions worked for me. Even adding two new user controls with no code or children would fail when trying to inherit one from the other.

The designer always calls the user control's parameterless constructor New and the control's Load event handler (provided that they exists), thus these methods should not do any operations that depend on some run-time resources.

I was quite puzzled when even added the two new plain user controls failed, as they of course had no constructor or load event handler. Then I noted that I could no longer open any user controls or forms in the whole solution. The VS designer had gone into a zombie state. A restart of Visual Studio, plus clearing the TFS workspace cache, corrected the VS designer problem for all the forms and the non-inherited user controls. Before I even tried to add a derived user control again, I did the recommended rebuild (see error message above) to ensure that coding errors did not cause the problem.

This time I added a standard user control and manually added the Inherits statement to the class code file and the designer partial class file. Then I opened the user control in designer mode, and I still got the famous designer error page, but this time with useful error details pointing me to a specific line in the base class.

The offending line was a private class member that created a biz-logic component using the abstract factory pattern:

Public Class GenericUCBase
. . .
Private _bizManager As IBizManager = ObjectFactory.BizManager()
. . .

Protected Sub New()

' This call is required by the Windows Form Designer.
InitializeComponent()
End Sub
. . .

The factory uses a Public Shared (static) method that creates objects using the dependency injection pattern (interface based) from pre-compiled assemblies deployed to the VS project location. It was the Type.GetType(typeName as String) method that failed when called from the private member initialization. I don't know why the VS designer failed to locate my assemblies during initialization, VS has no problems with non-derived controls, or with running or debugging the project.

All class members that are "declare initialized" gets called when the designer loads a user control, along with the constructor and the load event handler. Knowing this, I moved the initialization of the biz-logic component to my InitControl method, and this solved the VS designer problem.

The TFS workspace cache is located here:

\Documents and Settings\%user%\Local Settings\Application Data\Microsoft\Team Foundation\1.0\Cache

I clear it whenever I have problems with VS or TFVC, typically when the 'pending check-ins' list contains bogus entries from other solutions that cannot be removed in any other way.

Wednesday, December 21, 2005

VS2005 Design-time set DataSource error III

Design-time data-binding in .NET 2.0 WinForms is a real productivity booster that I use extensively. I have, however, had some problems with using the new object binding source mechanism (error I, error II) and today yet another problem. I began designing a new user-control a few days ago, and being a lazy developer, I copied some data bound comboboxes that I needed from another user-control.

I should have know better, copying controls was a recipy for immediate disaster in the VS2005 betas; you could be sure that the Visual Studio designer would fail when you later on re-opened the user-control. The same applied to renaming a control after modifying some of its properties. These problems have been fixed in the release version of VS, although opening a form still randomly causes the cursor to go into a blink frenzy for quite some time while re-syncing with the .Designer file.

Copying those combos kept some of the data binding properties (ValueMember, DisplayMember), but did not copy the binding sources (more on this below). I added the object binding sources in the 'Data Sources' explorer, and after some refreshing of the project data sources (error II), object binding seemed to work properly.

Today, I added another object binding source to the 'Data Sources' explorer of the user-control. If I then tried to use the DataSource property at design-time, I got this error:

Object reference not set to an instance of an object

Setting the data binding at run-time work fine. So does copying the original data source binding code from the hidden .Designer file, it even shows correctly in the control properties explorer, but cannot be changed due to the above error. But this is not how I want to do it, I wanted design-time binding to work. Any attempt to modify data binding on any controls in the form would cause the design-time error.

In an attempt to fix the binding problems, I used an old trick when working with software and computers; I restarted Visual Studio, opened the solution and cleaned it. Then I clicked the DataSource property of my failing combo. Now I got this error, which anyone who have parsed XML will recognize:

Root element is missing

After Visual Studio had shown this puzzling error (as usual no intelligble info provided in the message), all design-time data binding worked properly again and the combos were filled at run-time. Oh joy! . . . But the object binding source that I had added lastly, was gone from the 'Data Sources' explorer. As long as the design-time binding kind of worked again, I could live with that.

I suspected that there was a problem with one XML files in the solution and checked the various .RESX files and some other files with XML content, but I was not been able to find any error. When I had just about given up, I noticed that the three listed data sources in the 'Data Sources' explorer also happened to be the three first .datasource files in the \My Project\DataSources\ folder (use 'Show all files' in the 'Solution Explorer'). Thus, I opened the fourth .datasource file, and voila, it was empty! Visual Studio clearly just stops looping through the data sources when it encounters an error.

I excluded the empty file from the project, reloaded the solution, and then all the project data sources were listed in the 'Data Sources' explorer. Design-time binding now works as expected again. Oh, even more joy!

I am still sceptical to copying object bound controls from one WinForms user-control to another. The VS designer might punish you. Maybe I am just being paraniod.

Monday, December 19, 2005

VS2005 Crash on binding to data source II

Today I got a problem with VSTS 8.0.50727.42 that caused Visual Studio to just die, sometimes even without opening the error reporting dialog. This happened when trying to bind a DataGridView to a newly added object data source by using the popup toolbox of the grid. VS would vanish when I clicked the 'Choose data source' combo. No error message.

As I have had problems with the VS2005 data source mechanism before (data source crash I), I have some experience in troubleshooting data sources. I stared out checking for invalid, zombie data sources in the \My Project\DataSources\ folder (VB.NET), and removed an old, unused data source. This did not solve the problem, but I prefer to diagnose a clean solution.

When I made the new business entity object that I was adding as the new binding source, I also refactored some of the other entity objects to remove some obsolete properties and changed some property names to reflect gained knowledge about the domain.

I found out that VS2005 is not fond of such changes in the assemblies used as object binding sources. It is however, quite easy to make the data sources reflect the changes:
  • Select the project in the solution explorer
  • Click the 'Show all files' button
  • Navigate to 'My Project' and expand it
  • Navigate to the 'DataSource' child node and expand it
  • Select each of the data sources in turn
  • Right-click the data source and select 'Refresh'
Note that there is no refresh option on the 'DataSources' node in solution explorer, you have to manually refresh every single data source in your solution.

After refreshing the data source definition cache as described, VS2005 no longer performs harakiri in response to refactoring objects used as data sources.

Saturday, November 19, 2005

Noogen.Validation - WinForms validation made easy

After doing mostly ASP.NET and SharePoint solutions, I was quite pleased with the validation mechanisms of ASP.NET. I was very surprised and disappointed when I moved to developing WinForms solutions, and had to downgrade from the ASP.NET validator and validation summary controls to the WinForms stuff.

Gone were the validators and I had to use an ErrorProvider on my forms, as if I need something to provide me with errors. The worst was the need for iterating recursively over all controls on a form when clicking OK to ensure that all error had been corrected and that validation events returned success, before e.g. calling my biz-logic to save changes. I just wanted to have my Page.IsValid property back.

At my current project we agreed that the standard validation mechanism was to awkward for us, and one of the developers did some research to find a component that would make WinForms validation as simple as WebForms validation. This lead us to Noogen.Validation at CodeProject, a control that we have been using for some time now.

I just love the simplicity and flexibility of the Noogen.Validation component, and I strongly recommend it. It is the best add-on component I have used since the Farpoint Spread control for VB6. Thanks, Noogen!

Friday, November 11, 2005

VS2005 Add new data source wizard crash

Today I had a strange problem with VSTS 8.0.50727.42: I could not add a new data source of type 'object' to my Windows control library project. Adding a database or web-service data source worked fine.

The wizard would crash when trying to open the 'Select the object you wish to bind to' page of the wizard. The error message was this:

An unexpected error has occured.
Error message: Object reference not set to an instance of an object.


If I made a new Windows control library project and added classes from my business entity assembly as object data sources, the wizard worked as expected (kind of stupid that several classes cannot be added in one go, though). After several hours of experimenting with project references, structure, and even names, I finally saw the pattern of when the wizard worked and when it crashed. The wizard is dependent on your project having at least one public class in the root folder of your project.

In my Windows control library project I have structured the source code into several folders with no classes in the root. VS by default generates a class called 'UserControl1' in the root, and if you delete this class, the wizard will fail.

I now use a dummy class 'XDummyClassForDataBinding' in my project.

Thursday, October 13, 2005

.NET2.0 ObjectDataSource and BindingList<T> VS typed datasets

I am currently at a project where some VB6 and .NET1.1 WinForms applications are to be ported and upgraded to .NET2.0. The current architecture is layered and employs (custom) business entity objects modelled on the domain of the customer. The solution is distributed over several tiers, and the communications technology of the architecure (.NET remoting) is also up for review. In addition, it is a design goal to enable the solution to provide services and entity data to external systems, both within the company and to e.g. partners.

Yesterday some of the developers started to argue for replacing the existing business entity objects with typed datasets. Their main reason for this was to get "maximum" developer productivity through two-way WinForms data binding in the user controls. This is a typical way of thinking for developers that have worked mainly with databases and ASP.NET solutions. In addition, scrapping the domain entity objects would be a step in the wrong direction from 'domain driven design' (DDD) and from 'service orientation' enabling the solution. This discussion is not a new one, refer to this MSDN article by Dino Esposito for pros & cons of datasets vs business entity objects. I also recommend reading the articles referenced at the end of the 'Cutting Edge' article. Note how they all agree on that exposing datasets in a service is a bad idea.

.NET2.0 provides several new mechanisms aimed at bringing the ease of dataset binding to business entity objects, allowing the developers to use design time tools and wizards to bind their GUI to entity objects. The ObjectDataSource (BindingSource) and the generic BindingList<T> are the main enabling object binding mechanisms in .NET2.0. Note that it is possible to provide object binding also in .NET1.1 as shown in this MDSN article by Paul Ballard, but it will require more coding and will not give full design time support for setting up the binding.

What you will find out is that the BindingList<T> has some 'last mile' problems when compared to the binding mechanisms provided by the DataView/DataSet combination. In our prototype/spike the lack of built-in implementation for sorting and filtering immediately surfaced. These methods are defined in the IBindingList interface, but is not implemented by BindingList<T>. A bit of googling led me to this blog entry and this GotDotNet workspace. Andrew Davey's BindingListView<T> provides a data bindable view for a BindingList, in the same way that a DataView provides a bindable view of a DataTable. This component implements much of the stuff described in Paul Ballard's article. One less argument for the typed dataset clan :-)

Typed datasets still has the upper hand when it comes to batch updates, optimistic concurrency, etc, when compared to custom business entity objects and collections. What the solution might need is an ORM framework that handles all the object-to-database stuff (the DDD repository pattern). I might be in for a classic turf war with the dataset-all-the-way clansmen!