views:

825

answers:

5

This one has been stumping me for a while. But I'm no expert. This is a bit long...

I have a WinForms app with an Outlook style UI. That it to say there is a bar on the left hand pane that allows you to select a 'screen' which is a WinForms control, say the customer screen, and on the right hand pane there will appear a list of customers (i.e. the customer control). I call this the explorer interface. Double clicking a record will bring up a non-modal customer record in an additional window in the same way you would open an email in Outlook, we call this the inspector. If you double click multiple records you get multiple inspectors.

The whole thing is done using databinding. There is a BindingSource control on the customer list control and there another on the customer inspector. The customer control news up a static DataContext in it's load event and assigns the result of a simple Linq-To-SQL query to the BindingControl datasource property. When the customer list is double clicked the event looks up the record, casts it to a Linq-To-SQL customer object, and supplies this to the constructor of the customer inspector form. The customer inspector gets the customer object and assigns the datasource property of it's BindingSource control to it.

Since the BindingSource control supports IBindingList the contents of the customer object are not modified until EndEdit is called, in this app when the OK button is clicked. Since Linq to SQL implements the INotifyPropertyChanged interface the customer list is then updated. Cool.

However the problem comes when I want to refresh the content of the customer list to pick up changes made by other users, which I want to happen at regular intervals, every 60 seconds say. If I have a timer and re-run the query on the same datacontext no changes are picked up, because Linq to SQL does not want to squash any changes that have been made to the data under control of the datacontext. Strangely it runs the query against the DB but the identity tracking means that only new customer objects returned from the DB are added to the list. If I new up another datacontext then any open customer inspectors are no longer using the same datacontext, so any future changes to the open customer inspector are not reflected in the customer list.

Basically the data is stale because I am not using the Unit of Work pattern. So (if you are still with me), the questions are.

1. How the hell do I make the unit of work pattern work in this situation? It's easy enough is ASP.NET to use a request scoped datacontext, it's short lived, but in WinForms?

2. Are there any other ORMs that would work better in this scenario? NHibernate, EF, LLBLGEN etc.

3. How else should I go about it?

And also.

4. If I can make Linq to SQL work like this has anyone implemented IBindingList in the Linq to SQL partial classes which would avoid me having to use IBindingSource controls. (I'm not sure I should care about this).

5. If I can make Linq to SQL work like this is there any way of using SQL Notifications in SQL 2008 so that I can be notified when the underlying query results changes and requery then, rather than polling.

Thanks!

P.S. I am aware that I can use

db.Refresh(System.Data.Linq.RefreshMode.KeepChanges, customers)

but this causes a query to be run against the DB for each customer's record in the list.

A: 

I'm currently attempting to implement an identical scenario in our WinForms SmartClient app.

Did you come to a good solution with this?

In our app we have the same concurrent UI requirement, and the need to refresh the data from a shared source, however we are using WCF services instead of LinqToSql. I've implemented our own identity map and change tracking solution.

It's obviously important that changes made to your customer in the inspector are reflected in the other views. But this kind of gives the impression to the user that their changes have been saved.

I'd draw the line around the unit of work to start when you read the customer list, and end whenever the user's changes have been saved. If you re-read the customer list and there are no changes, that's a new unit of work. If there are changes then you'll need to close and save the changes the user has made before you can start a new unit of work.

Our issue is that we have many windows open at once, and it's easy for a user to make some edits and not commit them to the backend. Then it's not obvious why they can't reload the master list.

We decided in this case we should auto-save the user's changes.

Did you come up with any better solutions?

A: 

Perhaps a little old... But regarding points 4/5 be sure to check out Bindable LINQ project on CodePlex. Definitely some good code in there that solves exactly your problem.

http://www.codeplex.com/bindablelinq

Reddog
A: 

@Andronicus

After looking at tens of ORMas I am currently looking into the a comercial ORM called Genome (http://www.genom-e.com/).

I seems to allow me to do more of the above, like more advanced databinding, and records can be stopped from going stale but I am still finding my way though it. I'll let you know how I get on, but I might be sometime.

@Reddog

I looked at BindableLinq and I do really like it. But it's Linq to Objects and therefore has no Linq to SQL translations as far as I can tell. (Unless I'm wrong).

Many thanks!

Christopher Edwards
A: 

I have tried the similar thing, here is my two cents.

I don't think you can implement unit of work pattern here because of the way your UI works. As you may already know, LinqToSql DataContext is designed to be a light-weighted and short-lived object. It binds to a "unit of work" naturally. In your case, committing change(s) to DB is one unit, refreshing changes from DB is another unit. But you want one DataContext instance to do both.

Also, I am curious about what your UI is supposed to do when a user is editing one record while another user just commit some changes on the same record to DB. How do you handle such concurrency conflicts from UI point of view?

You may have to make some compromise on the UI. One way to do it is to make customer detail view have two mode "showing" and "editing". Showing is a simply read-only view with a timer refreshing in some interval. Editing is a snap shot which allow users to modify the data but have no idea of other people's updates. In the end, when users commit the updates, let optimistic concurrency handle the conflict. Users can't see the real time changes when they are editing.

Your point #5 is interesting. What we did was having something doing nothing but querying DB to gather the newest updates either based on an interval or some signals. We called it "publishing service". For this to work you will need to have a timestamp column in your DB table.

With the "publishing service" in place, you can get the delta set (updates and new records) without using the DataContext in the customer control. If you "merge" the delta set to your local DataBinding DataSource, your customer detail view should refresh. Now, the DataContext instance in the customer control is dedicated to updating. You can choose to let user decide when to commit. Or you can make it commit whenever user's focus leaves the row (during validating). Personally I would do the latter because I don't feel like having a DataContext live for an unpredicted amount of time.

Kai Wang
I don't think DB concurrency is handled differently than it would be in any other system particularly. For the record I'm just doing last save wins. The user who opens the second record will have out of date data if the first user saves their changes whilst they are looking at the inspector, if the second user changes the data this will generate a concurrency exception which I just acknowledge and overwrite the first users data (if the user says yes to an overwrite data message).
Christopher Edwards
+2  A: 

I'm going to restate your problem to make sure I've understood it.

You have a widget which presents a list of entities (the LIST). When you click an item in the LIST, another widget appears which allows the user to edit the entity. When the user has finished editing the entity, their changes are committed to the DB, and should also be reflected in the LIST of entities. Periodically, the system should also fetch changes that other users have made to items in the LIST and update the LIST.

If this is correct, I'm going to leave aside any concurrency problems of two users editing the same entity as this does not seem to be your concern, and will focus on how to organise the UI and the Unit of work.

You need to separate out the entities in your LIST from your entities being edited by your inspectors. The business process represented by your inspectors are your units of work, one unit for each entity. Your list does not represent a unit of work. It is a stale representation, or point in time, of the combined work of all the previously committed units of work. Your LIST doen't even have to deal with your entities directly, it can hold an ID or any other way for your inspectors to get at the underlying entity from the DB when a user clicks it. Now you will be free to update the list whenever you like as your inspectors don't share instances with it at all.

To simulate to your user that when they are editing an entity via an inspector and to make them appear to be bound to the same thing you have two choices.

1) list is only bound to committed data in the DB. This is easy, when an inspector flushes local changes back to the database and commits successfully, provide a way for the inspector to tell the list to update itself.

2) list is bound to committed data + local un committed data. This is a bit harder, you need to expose methods on your list which allow an inspector to trump data coming back from the Db, and overwrite it with its own local dirty data.

Noel Kennedy
Yes, I have had thought along these lines. Make the list a read-only list that is refreshed from the DB with a DataContext that has it's object tracking set to false. Then let the inspectors do the unit of work when constructed with an ID parameter. I'll give it a try.
Christopher Edwards