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 16, 2006

Integrating Team-Sites into MSCRM

We usually implement SharePoint (both WSS and SPS) at our MSCRM customers to provide collaboration and document archive functionality. We use the iframe-style presentation layer integration mechanism that MSCRM provides through the ISV.CONFIG.XML file (ISV.CONFIG in v1.2). Microsoft has even provided an XSD schema for this configuration file in v3.0. You will find these files in the \_Resources\ folder underneath the install directory of MSCRM.

I will explain what you need to do to integrate WSS team-sites into MSCRM, and which modifications you should make to the web-part pages (WPP) and document libraries (doc-libs) of a team-site to make them well-behaved parts of MSCRM.

Our MSCRM-WSS integration is based on using some intermediate "staging" .ASPX pages that are the targets for e.g. the MSCRM <NavBarItem> element's URL attribute. We typically used two pages, one for doc-libs and one for other collaboration features of SharePoint:

<!-- The Account Left Nav Bar -->
<NavBar ValidForCreate="0" ValidForUpdate="1">
<NavBarItem Icon="/_imgs/ico_18_1.gif" Title="Document Archive" Url="http://london/Objectware.Mscrm.SharePoint/TeamSiteSharedDocuments.aspx" Id="Account.SharedDocs"/>
<NavBarItem Icon="/_imgs/ico_18_9.gif" Title="Collaboration" Url="http://london/Objectware.Mscrm.SharePoint/TeamSiteSummary.aspx" Id="Account.ViewTeamSite"/>
<NavBarItem Icon="/_imgs/ico_18_9.gif" Title="Links" Url="http://london/Objectware.Mscrm.SharePoint/TeamSiteSummary.aspx" Id="Account.ViewLinks"/>
</NavBar>

Note that the PassParams attribute of v1.2 is now obsolete, and that the object type and guid now always get passed to the target page. Note the Id attribute; it is central in making the integration flexible. Use the attribute to pass different "tokens" to the staging-page to allow the page to provide different parts of the SharePoint team-site as the response. The parameters are passed as a querystring. Get the passed data in the Page_Load event of your .ASPX page:

string guid = Request.QueryString["oId"];
string type = Request.QueryString["oType"];
string tabSet = Request.QueryString["tabSet"];


Note that
the Id attribute has the key tabSet in the URL querystring. Note also that the tabSet value gets the suffix "area", e.g. Account.ViewLinks becomes Account.ViewLinksarea. The oId value is the entity record GUID and the oType value is the type of the entity. Some of the most used types are:
  • Account = 1
  • Contact = 2
  • Opportunity = 3
  • Incident (case) = 112
Now that you know exactly which entity that requested your staging-page to show which specific WPP of a team-site, all you need to determine is which team-site it is, whether it exists, and whether the requesting user has access to the page (i.e. is a team-site member). Deducing the team-site URL from the parameters is left as an exercise for you, but a simple approach is to use the entity GUID a the only varying part of the team site URL, for example:
http://wss-server/sites/GUID/default.aspx

We also use a team-site generator made by Mads Nissen to allow a user to create new team-sites directly from inside MSCRM should the team-site not exists. The method shown below is used to check whether a team-site exists or not for e.g. a specific account. Lately, we have started using K2.net workflow to get more flexibility and power in the team-site creation process.

Your staging-page should use <identity impersonate="true"> in the relevant WEB.CONFIG file to ensure that you assert team-site access as the logged on MSCRM user. You could use the SharePoint object model to check if the site exists and if the user is a member. I have chosen the simpler method of using the HttpWebRequest of the System.Net namespace and checking for exceptions, making some simple assumptions:

public static void CheckTeamSiteUrl(string url)
{
string response = "";
HttpWebResponse httpResponse = null;

//assert: user have access to URL
try
{
HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(url);
httpRequest.Credentials = System.Net.CredentialCache.DefaultCredentials;
httpResponse = (HttpWebResponse)httpRequest.GetResponse();
}
catch(Exception ex)
{
throw new ApplicationException("HTTP 403 Access denied, URL: " + url, ex);
}

//if here, the URL is correct and the user has access
try
{
StreamReader stream = new StreamReader(httpResponse.GetResponseStream());
response = stream.ReadLine(); // .ReadToEnd();
stream.Close();
httpResponse.Close();
}
catch(Exception ex)
{
throw new ApplicationException("HTTP 404 Page not found, URL: " + url, ex);
}

if(response.ToLower() == "<html><body>the web site that is referenced here is not in the configuration database.</body></html>")
{
throw new ApplicationException("HTTP 404 Team-site not found, URL: " + url, null);
}
}


Note that WSS won't respond with a proper HTTP 404 error when the team-site does not exist. Rather, WSS responds with a valid page with a single line of text explaining that there is no such team-site in the SharePoint database.

If the CheckTeamSiteUrl method returns failure, a new team-site is generated from a WSS template (.STP) using the standard SharePoint web-services. The WSS template contains one or more web-part-pages (WPP) tailored for integration into MSCRM (see below).

If the
CheckTeamSiteUrl method returns success, do a simple redirect to the actual target page within the WSS team-site.

You should tailor the WPPs that you integrate into MSCRM for these reasons:
  • To remove the SharePoint "chrome": i.e. the page header, top menu, search, left menu, etc
  • To configure the custom MSCRM view(s) of the doc-lib to use only basic column types rather than the "edit" column types, to keep the navigation options simple and controllable
  • To configure doc-lib toolbars to be suitable for inline navigation in MSCRM
  • To ensure that all hyperlinks are suitable for navigation within the MSCRM frame: if a link is not suitable for inline navigation, apply a target="_blank" attribute to it
How you create a new WPP depends on wether it is a new custom team-site page or a new custom view for an existing document library. To create a new team-site page, open the WPP you want to integrate into MSCRM in FrontPage2003 and save it as a new web-page for use in MSCRM. To create a custom view for a doc-lib, I recommend using the standard 'Modify settings and columns-Views-Create a new view' tool. Tailor the new view to be suitable for inline navigation in MSCRM. It is better to do all customizations before hiding the SharePoint "chrome", as hiding the chrome also hides access to the customization tools.

I prefer to use the prefix MsCrmView_ on all web-part-pages tailored for integration into MSCRM. Use the 'split' view of the WPP to select the parts to remove, and apply a style="display: none;" attribute to the <TR> and <TD> elements to hide them. Do not delete them from the page, some SharePoint JavaScript might depend on the elements being part of the HTML DOM.

As a web-part-part page is built at run-time from the web-parts residing in the web-part-zones of the page, you need to device a JavaScript that runs on page load 'complete' event to modify their navigation behavior (the target attribute). This script
is left as an exercise for you.
[UPDATE] The hyperlink modification JavaScript can be found here, including further details.

Do not hesitate to remove the SharePoint "Modify shared page" menu link. The toolbox can always be summoned using this querystring:
?mode=edit&PageView=Shared&toolpaneview=2 (see SharePoint tweaks).

Also try this nifty little SharePoint querystring trick: ?contents=1



Wednesday, February 08, 2006

Conditional XML serialization

In my post about implementing entity object row state using 'is dirty' and 'cloning' I described how to exclude properties during binary serialization using [NonSerialized]. The same can be done with XML serialization using [XmlIgnore]. In addition, properties can be temporarily excluded when serializing to XML. The mechanism behind conditionally excluding properties from XML serialization is not well known, thus this post to shed some light on it.

The bool <property name>Specified mechanism is the .NET serialization framework answer to XSD minOccurs="0" applied by code generators such as XSD.EXE, XsdObjectGen, and others. Most developers actually implicitly use this mechanism when they design and use typed datasets with optional fields (columns).

In short, whenever .NET serializes the properties of a class to XML, it always checks to see if there exists a boolean metod whose name is the name of the property suffixed with Specified. If such a method exists, and it returns false, the property is not included in the serialized XML; if it returns true, the property and value is serialized. The oppsite logic is applied on de-serialization of optional XML elements.

Thus, you can implement and use Specified methods at will to get conditional XML serialization in .NET. The serializer does not discriminate between methods implemented by code generators and methods implemented by you. Partial classes in .NET 2.0 also makes adding such methods to code generated classes much simpler.

I got to know the details of how XSD.EXE handles XSD minOccurs="0" and xsi:nil="true" very well when doing "web-service contract first" in combination with InfoPath last year. The support for doing WSCF in .NET 2.0 has improved with the support for e.g. interface based .ASMX web-services, but Visual Studio 2005 still has a way to go to support WSCF to its maximum extent. I expect that this will be finally solved when WCF (Indigo) is released.

Sunday, February 05, 2006

Implementing "RowState" for Entity Objects

At my current project, we have some object model design discussions about "the how" for our entity objects (whose primary objective is "the what" of the domain), between the DataSet clan and the POCO clan. Our system is quite big, and must be designed to make it easy to provide future services to other parts of the organization (including Java-based systems) and to external partners. Support for web-services is a planned feature of the system, not to mention SOA.

Personally, I always like to design a system in a way that make the client application just another consumer of the system services. This promotes re-usability and low coupling, and is best achieved through a TDD approach. Thus, as you may guess, I am not in favor of using DataSets as the basis for entity objects, value objects, messages, or for DTOs. I recommend reading Scott Hanselman's famous blog post about using DataSets as business objects or as message elements in web-services. Also check out this MSDN Mag article, including the referenced blog posts at the end of the article.

Design-time data-binding of anything but DataSets was not simple in .NET 1.1, but with the advent of the object binding source and generics (List<T>) in .NET 2.0, it has become viable to do data-binding to entity objects and lists/collections.

The software design discussion now revolve around the RowState to signal the state of an entity object in relation to a containing list. Having a RowState property is really useful when e.g. implementing the 'Unit of Work' pattern. I have promoted that the 'cloning' and 'is dirty' mechanisms, plus a deleted flag as enough, while the DataSet clan advocates the need for a separate, setable row-state property, which will introduce some ambiguity in deducing the actual state of an entity object when received from a client.

I claim that the RowState need not be a separate, serializable property, it is just a read-only combination of the entity object identifier .Id, .IsDirty, and an .IsDeleted flag (the latter being the only setable property, the other two are read-only to the clients). Each entity object must have a class member that contains a clone of its original state. The .Id (internal, hidden entity identifier; PK) is assumed to be e.g. -1 when the object is not an existing entity in the data store. The state of an entity object is deduced like this:

  • Added: .Id == -1 && .IsDirty == true && .IsDeleted == false
  • Deleted: .IsDeleted == true && .Id != -1 (must exist to be deleted)
  • Detached: not applicable, entites are not defined by membership in any List<T>
  • Modified: .Id != -1 && .IsDirty == true && .IsDeleted == false
  • Unchanged I: .IsDirty == false && .IsDeleted == false
  • Unchanged II: .Id == -1 && .IsDeleted == true (aborted insert, no operation on this operand)
Thus, RowState is just a read-only property on the entity object. If your system requires the need for public setable row-state (like the new feature in ADO.NET 2.0), I recommend that you add an AdviceRowState property that the system clients can use to signal their intended state of the entity object.

Remember to keep any "how" properties away from the serialization of the "what" when using serialization as the basis for .IsDirty, otherwise such extra properties will cause bogus 'is dirty' logic. This includes any 'original values' clone that you may include in the entity object to support the .IsDirty method. As the 'is dirty' code uses the BinaryFormatter, use the [NonSerialized] attribute on all fields/class members that should not be comprised by the .IsDirty comparison.

You may also need to exclude some properties from XML serialization e.g. to prevent exposure in your web-services.
Apply the XML serialization control attribute XmlIgnore to such properties. You may also conditionally serialize properties such as the AdviceRowState property to XML. Add an extra control property bool AdviceRowStateSpecified and set it to false to exclude the actual property from XML serialization. Apply [XmlIgnore] to the AdviceRowStateSpecified property as you do not want it to be serialized anyway.

It should always be an internal aspect of your system how domain entities are stored, how you implement locking, how you implement long-running work/operations, etc ( see Data on the outside vs. Data on the inside by Pat Helland). The AdviceRowState property is just for giving the client the perception of having a say. The operations provided by your system should always make the client say please: "please update the account with this data", "please delete this order", etc.

The WinForms DataGridView still favors the use of DataSets as it has built-in support for DataTable.DataRow.RowState, e.g. automatically hiding deleted rows. This will not happen when using an object binding source bound to a generic list. Using two List<T> are one way of solving this, one for the deleted entities and one for the others. Use the latter as an object binding source, and the former to keep track of deleted entities. Pass both lists in the message to your service to save the changes (the lists are the 'operands' of the message).

Thus, the RAD model of Visual Studio favors developer productivity over good software design. Read 'Does Visual Studio Rot the Mind?' by Charles Petzold for more on this topic.

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.