15
\$\begingroup\$

I have a WCF client which passes Self-Tracking Entities to a WPF application built with MVVM. The application itself has a dynamic interface. Users can select which objects they want visible in their Work area depending on what role they are in or what task they are doing.

My self-tracking entities have quite a few Navigation Properties, and a lot of them are not needed. Since some of these objects can be quite large, I'd like to only load these properties on request.

My application looks like this:

[WCF] <---> [ClientSide Repository] <---> [ViewModel] <---> [View]

My Models are Self-Tracking Entities. The Client-Side Repository hooks up a LazyLoad method (if needed) before returning the Model to the ViewModel that requested it. All WCF Service calls are asyncronous, which means the LazyLoad methods are also asyncronous.

The actual implementation of the LazyLoad is giving me some trouble. Here are the options I have come up with. I apologize for the walls of text, they're options with code samples.

Option A

LazyLoad the Model's properties upon request in the Getter

[DataMember]
public TrackableCollection<ConsumerDocument> ConsumerDocuments
{
    get
    {
        if (_consumerDocuments == null && !IsDeserializing && !IsSerializing)
        {
            _consumerDocuments = new TrackableCollection<ConsumerDocument>();
            _consumerDocuments.CollectionChanged += FixupConsumerDocuments;
            LoadConsumerDocuments();
        }
        return _consumerDocuments;
    }
    set { /* Default T4 STE Set removed for space */ }
}

private async void LoadConsumerDocuments()
{
    var obj = await LazyLoadData("ConsumerDocuments", new object[] { Ident });
    if (obj != null && obj is IEnumerable<ConsumerDocument>)
    {
        ConsumerDocuments.AddRange((IEnumerable<ConsumerDocument>)obj);
    }
}

Good: Loading data is so simple it's not worth mentioning. Creating a binding in your XAML loads the data when needed, and skips it when not. For example, <ItemsControl ItemsSource="{Binding CurrentConsumer.ConsumerDocuments}" /> will load the data, however if the Documents section of the interface is not there then nothing gets loaded.

Bad: Cannot use this property in any other code before it has been initiated because it will return an empty list. For example, the following call will always return false if documents have not been loaded.

public bool HasDocuments 
{ 
    get { return ConsumerDocuments.Count > 0; }
}

OPTION B

Use the default GET for the property and manually make a call to load data when needed

Good: Can use these properties anywhere

Bad: Must remember to load the data before trying to access it. This might seem simple, but it can get out of hand quickly. For example, each ConsumerDocument has a UserCreated and UserLastModified. There is a DataTemplate that defines the UserModel with a ToolTip displaying additional User data such as extension, email, teams, roles, etc. So in my ViewModel that displays documents I would have to call LoadDocuments, then loop through them and call LoadConsumerModified and LoadConsumerCreated. Basically anything that is displayed in a UI binding needs to be Loaded first.

OPTION C

Create two ways to access the property. One for the binding, and one for any other code

// Properties used in Bindings
[DataMember]
public TrackableCollection<ConsumerDocument> ConsumerDocuments
{
    get
    {
        if (_consumerDocuments == null && !IsDeserializing && !IsSerializing)
        {
            _consumerDocuments = new TrackableCollection<ConsumerDocument>();
            _consumerDocuments.CollectionChanged += FixupConsumerDocuments;
            LoadConsumerDocuments();
        }
        return _consumerDocuments;
    }
    set { /* Default T4 STE Set removed for space */ }
}

private async void LoadConsumerDocuments()
{
    var consumerDocuments = await GetConsumerDocuments();
    if (consumerDocuments != null) 
        ConsumerDocuments.AddRange(await GetConsumerDocuments());

    IsConsumerDocumentsLoaded = true;
}

// Method used for non-binding code calls
public async Task<TrackableCollection<ConsumerDocument>> GetConsumerDocuments()
{
    if (IsConsumerDocumentsLoaded)
    {
        return ConsumerDocuments;
    }
    else
    {
        // Do something like call Getter to initialize collection
        if (this.ConsumerDocuments == null) { }

        var obj = await LazyLoadData("ConsumerDocuments", new object[] { Ident });
        if (obj != null && obj is IEnumerable<ConsumerDocument>)
        {
            return new TrackableCollection<ConsumerDocument>((IEnumerable<ConsumerDocument>)obj);
        }
    }
    return null;
}

Good: Data is loaded asynchronously as needed - Exactly what I want.

Bad: Having two ways to access the same data seems inefficient and confusing. You'd need to remember when you should use Consumer.GetConsumerDocuments() instead of Consumer.ConsumerDocuments and vise versa. There is also the chance that the WCF Service call gets run multiple times.

OPTION D

Skip the Asyncronous loading and just load everything synchronously in the setters.

Good: Very simple, no extra work needed

Bad: Would lock the UI when data loads. Don't want this.

OPTION E

Have someone on SO tell me that there is another way to do this and point me to code samples :)

Other Notes

Some of the NavigationProperties will be loaded on the WCF server prior to returning the object to the client, however others are too expensive to do that with.

With the exception of manually calling the Load events in Option C, these can all be done through the T4 template so there is very little coding for me to do. All I have to do is hook up the LazyLoad event in the client-side repository and point it to the right service calls.

\$\endgroup\$
  • 2
    \$\begingroup\$ Just passing right now but initially what I'll say immediately is don't use properties that have side effects, and if a method is asynchronous then explicitly mark it so by naming convention using BeginX or XAsync, you could even use XTask but that's a little weird when you think about it - decide on something and stick to it. Remember that while you might know this code intimately, your contemporary colleagues and/or successors and users might not. \$\endgroup\$ – Grant Thomas May 4 '11 at 16:46
  • 2
    \$\begingroup\$ Forgive me if I am missing something critical here, but the WCF proxy types generated won't have any concept of navigational properties, or lazy loading. At the point of serialisation, those properties would be read and the resultant data passed to the client, no? \$\endgroup\$ – Matthew Abbott Jun 3 '11 at 6:44
  • \$\begingroup\$ We are currently facing a performance problem in a Web application with STE over WCF and thinking about implementing Lazy Loading. As a matter of fact, I would like to know wich option you've picked up in the end and why ? Also, if you could provide a code snipett of your T4 template, it would be very helpfull. \$\endgroup\$ – valtrem Oct 18 '11 at 21:08
  • \$\begingroup\$ I ended up creating two public properties for the lazy-loaded data: a Sync version and an Async version. They both reference the same private property, so no extra data was transferred to/from the WCF server. You can view the following SO answer for additional information: stackoverflow.com/questions/5875271/… I'm not 100% happy with it, but the project I was using it on got cancelled shortly after, so I never got to revisit it and improve it. \$\endgroup\$ – Rachel Oct 19 '11 at 1:47
  • \$\begingroup\$ Also, I'm not sure I still have access to the T4 template, but what part of it were you interested in? \$\endgroup\$ – Rachel Oct 19 '11 at 1:48
5
\$\begingroup\$

I had cross-posted this question on SO, and got some good answers there. The answer I ended up going with was my own, although it was based on some of the other answers given to the SO question.

Here's a copy of the answer I ended up going with:


The solution I came up with was to modify the T4 template for the self-tracking entities to make the changes shown below. The actual implementation has been omitted to make this easier to read, but the property/method names should make it clear what everything does.

Old T4 Generated Navigation Properties

[DataMember]
public MyClass MyProperty { get; set;}

private MyClass _myProperty;

New T4 Generated Navigation Properties

[DataMember]
internal MyClass MyProperty {get; set;}
public MyClass MyPropertySync {get; set;}
public MyClass MyPropertyAsync {get; set;}

private MyClass _myProperty;
private bool _isMyPropertyLoaded;

private async void LoadMyPropertyAsync();
private async Task<MyClass> GetMyPropertyAsync();
private MyClass GetMyPropertySync();

I created three copies of the property, which point to the same private property. The internal copy is for EF. I could probably get rid of it, but its easiest to just leave it in since EF expects a property by that name and its easier to leave it than to fix up EF to use a new property name. It is internal since I don't want anything outside of the class namespace to use it.

The other two copies of the property run the exact same way once the value has been loaded, however they load the property differently.

The Async version runs LoadMyPropertyAsync(), which simply runs GetMyPropertyAsync(). I needed two methods for this because I cannot put the async modifier on a getter, and I need to return a void if calling from a non-async method.

The Sync version runs GetMyPropertySync() which in turn runs GetMyPropertyAsync() synchronously

Since this is all T4-generated, I don't need to do a thing except hook up the async lazy load delegate when the entity is obtained from the WCF service.

My bindings point to the Async version of the property and any other code points to the Sync version of the property and both work correctly without any extra coding.

<ItemsControl ItemsSource="{Binding CurrentConsumer.DocumentsAsync}" />

CurrentConsumer.DocumentsSync.Clear();

As a side note, I wasn't thrilled with this solution once in place because I kept forgetting to add the Sync/Async suffix in the XAML, and would have preferred to have a single property name that does both, but I don't think that's possible.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.