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.
BeginX
orXAsync
, you could even useXTask
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\$