views:

9337

answers:

5

I'm developing a business application, using Silverlight for the UI and a WCF webservice for the back-end. In the database I have a number of lookup tables. When the WCF service returns a business object, one of the properties contains the entire row out of the lookup table instead of just the foreign key, so in the UI I can display things like the description from the lookup table without making another call to the service. What I am trying to do at the moment is provide a combobox bound to the entire list of lookup values and have it update properly. The business object I'm dealing with in this example is called Session and the lookup is called SessionType.

Below is the definition of the combobox. The DataContext is set to an instance of Session. I am setting an ItemTemplate because the combobox is displaying more than just a list of strings.

<ComboBox 
x:Name="SessionTypesComboBox"
ItemTemplate="{StaticResource SessionTypeDataTemplate}"
ItemsSource="{Binding Source={StaticResource AllSessionTypes}}"
SelectedItem="{Binding Path=SessionType, Mode=TwoWay}"
/>

Both the business object and the lookup table are being loaded asynchronously via the web service. If I do nothing else, the combobox list will be populated with SessionTypes, but it will not show the initial SessionType value from Session. However Session will be updated with the correct SessionType if the combobox selection is changed.

What seems to be happening is that the SelectedItem binding cant match the SessionType in Session to its equivalent in the SessionType list. The object values are the same but the references are not.

The workaround I have found is to load the Session and the SessionTypes list, then update the current SessionType of Session with the corresponding one from the SesstionTypes list. If I do that then the combobox displays correctly. However to me this has a bad code smell. Because everything is loaded asyncronously, I have to determine when everything is available. Here's how I'm doing that:

In the code-behind of my Silverlight user control:

// incremented every time we get data back during initial form load.
private volatile int m_LoadSequence = 0;

...

// Loaded event, called when the form is er... loaded.
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
    // load session types
    var sessionTypes = this.Resources["AllSessionTypes"] as Lookups.AllSessionTypes;
    if (sessionTypes != null)
    {
        sessionTypes.DataLoadCompleted += (s, ea) =>
        {
            IncrementLoadSequence();
        };
        sessionTypes.LoadAsync();
    }

    // start loading another lookup table, same as above
    // omitted for clarity

    // set our DataContect to our business object (passed in when form was created)
    this.LayoutRoot.DataContext = this.m_Session;
    IncrementLoadSequence();
}

// This is the smelly part. This gets called by OnBlahCompleted events as web service calls return.
private void IncrementLoadSequence()
{
    // check to see if we're expecting any more service calls to complete.
    if (++m_LoadSequence < 3)
        return;

    // set lookup values on m_Session to the correct one in SessionType list.

    // Get SessionType list from page resources
    var sessionTypes = this.Resources["AllSessionTypes"] as Lookups.AllSessionTypes;

    // Find the matching SessionType based on ID
    this.m_Session.SessionType = sessionTypes.Where((st) => { return st.SessionTypeID == this.m_Session.SessionType.SessionTypeID; }).First();

    // (other lookup table omitted for clarity)
}

So basically I have a counter that gets incremented each time I get data back from the webservice. Since I'm expecting 3 things (core business object + 2 lookup tables), when that counter gets to 3 I match up the references.

To me, this seems very hacky. I would rather see the combobox specify a ValueMemberPath and SelectedValue to match the selected item with one in the list.

Can anyone see a cleaner way of doing this? This situation is very common in business apps, so I'm sure there must be a nice way of doing it.

+1  A: 

I'm not sure I'm fully understanding the problem (it's early :)) But can't you just transfer all the items you need in one call? (even if you have to wrap the 3 in a new DTO class), then you can just update the current session type using a complete event. It's still not perfect, but at least you don't have to keep any counters.

I'd also move all that logic to a ViewModel and just bind to that, but that's just me :)

Steven Robbins
+1  A: 

You'd be better off binding to an ObservableCollection then using some other code (a View Model part of a MVVM isn't a bad choice) to update it in the background. That way you get separation from the UI and its a lot easier to handle the updates as the UI is just bound.

Shawn Wildermuth
+2  A: 

Geoff,

To confirm I understand your problem: the databinding infrastructure doesn't seem to recognise that two objects you consider 'equal' are actually equal - therefore the initial SelectedItem isn't set correctly since the databinding doesn't find a reference-equals object in your StaticResource collection to match Session.SessionType. You get around this by 'flattening' the references (ie. you force the Session.SessionType to be reference-equals in the Where((st)...First() code.

We have had a similar problem.

It does kinda of make sense that Silverlight won't automatically 'equate' two objects from difference 'sources' just because you know they represent the same data. Like you said "The object values are the same but the references are not". But how can you MAKE the databinding equate them?

Things we thought of/tried:

  • implementing .Equals() on the class (SessionType in your case)

  • implementing operator == on the class (SessionType in your case)

  • implementing IEquatable on the class (SessionType in your case)

  • making the collection only Strings and binding to a string property

but in the end we have given up and used the same approach as you - 'extracting' the correct reference-equals object from the 'collection' (after everything is loaded) and poking it into the SelectedItem-bound property.

I agree with you about the code-smell, and suspect there must be a better solution. So far all our debugging in the property accessors and no-op IValueConverters hasn't found a solution -- but if we do I'll post it back here too.

CraigD
A: 

Thanks for the answers, all of the above were useful! I'm moving towards the MVVM way as well as combining several service calls into a single one (also reduces round-trip overhead). Looks like I'll stick with the lookup re-referencing for the time being - if I find a better way I'll post it as well.

geofftnz
A: 

geofftnz, Have you find any nice solution for this?

CraigD, I doubt that overriding Equals etc is a good solution. First, this is to be done inside the generated proxy class SessionType, so these changes will be lost on each service reference update. Second, notification in the SessionType setter (here SessionType is the same generated client proxy class) uses ReferenceEquals call... so that's one more place to touch the generated code! OK, the first thing can be done via handmade partial class SessionType (and so will not be lost after updates), but the second thing certainly cannot be done the same way.

Comanche
trying to remember the original question - I've changed jobs since then :)
geofftnz
I seem to remember wrapping them all in one DTO... I also moved to a proper MVVM pattern.
geofftnz