Asynchronously Lazy-Loading Navigation Properties of detached Self-Tracking Entities through a WCF service?
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.
EDIT - I removed the code samples to try and make this easier to read and understand. See previous version of question if you want to see it
Option A
Asynchronously LazyLoad the Model's properties from the WCF server in the Getter
Good: Loading data on demand is extremely simple. The binding in the XAML loads the data, so if the control is on the screen the data loads asynchronsly and notifies the UI when it's there. If not, nothing loads. 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
Manually make a call to load data when needed
Good: Simple to implement - Just add LoadConsumerDocumentsSync()
and LoadConsumerDocumentsAsync()
methods
Bad: Must remember to load the data before trying to access it, including when its used in Bindings. 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
. It could keep going too... after that I'd have to LoadUserGroups
and LoadUserSupervisor
. It also runs the risk of circular loops where something like a User
has a Groups[]
property, and a Group
has a Users[]
property
OPTION C
My favorite option so far... create two ways to access the property. One Sync and one Async. Bindings would be done to the Async property and any code would use the Sync property.
Good: Data is loaded asynchronously as needed - Exactly what I want. There isn't that much extra coding either since all I would need to do is modify the T4 template to generate these extra properties/methods.
Bad: Having two ways to access the same data seems inefficient and confusing. You'd need to remember when you should us开发者_如何转开发e Consumer.ConsumerDocumentsAsync
instead of Consumer.ConsumerDocumentsSync
. There is also the chance that the WCF Service call gets run multiple times, and this requires an extra IsLoaded property for every navigational property, such as IsConsumerDocumentsLoaded.
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.
Gave it some thought, first of all I have to say that you must provide a clear to reader solution to this problem, DependecyProperties being loaded async when you bind to User.Documents property can be ok, but its pretty close to a side effect based solution. If we say that such behaviour in View is ok, we must keep our rest code very clear about it intentions, so we can see how are we trying to access data - async or sync via some verbose naming of something (method,classname,smth else).
So I think we could use a solution that is close to old .AsSynchronized() approach, create a decorator class, and provide each property a private/protected AsyncLoad & SyncLoad method, and a decorator class would be Sync or Async version of each lazyloadable class, whatever is more appropraite.
When you decorate your class with Sync decorator it wraps each lazyloadable class inside with a Sync decorator too so you will be able to use SynchUser(User).Documents.Count on sync class version with no probs cause it will be smth like SynchUser(user).SyncDocuments(Documents).Count behind in overloaded version of Documents property and would call sync getter function.
Since both sync and async versions will be operating on same object this approach wont lead to modifying some non referenced anywhere else object if you want to modify any property.
Your task may sound as one that can be solved in some magic "beautiful & simple" way but I dont think it can, or that it wont be any more simple than this one.
If this doesn't work im still 100% sure you need a clear way to differntiate in code whether a sync or async version of class is used or you will have a very hard to maintain code base.
Option A should be the solution.
Create one property named LoadingStatus indicating data is loaded or loading not yet loaded. Load data asynchronously and set the LoadingStatus property accordingly.
Check the loading status in each property and if data not loaded then call function to load data and viceversa.
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();
Could the Binding.IsAsync
library property be helpful here?
Edit: expanding a little.. Have a lazy loaded synchronous property that will call the WCF service on first use. Then the async binding will keep the UI from blocking.
While this question was asked a while ago, it is near the top of the async-await keyword list and I think would be answered quite differently in .net 4.5.
I believe this would be a perfect use case for the AsyncLazy<T>
type described on several sites:
http://blogs.msdn.com/b/pfxteam/archive/2011/01/15/10116210.aspx http://blog.stephencleary.com/2012/08/asynchronous-lazy-initialization.html http://blog.stephencleary.com/2013/01/async-oop-3-properties.html
I have two thoughts in my head.
1) Implement an IQueryable<>
response on the WCF
Service. And follow right down to the DB with an IQueryable<>
pattern.
2) In the client repository set the getter on the ConsumerDocuments
property to fetch the data.
private IEnumerable<ConsumerDocuments> _consumerDocuments;
public IEnumerable<ConsumerDocuments> ConsumerDocuments
{
get
{
return _consumerDocuments ?? (_consumerDocuments = GetConsumerDocuments() );
}
}
The way I see it, the ViewModel needs to be aware if there's data available or not. You can hide or disable UI elements that are meaningless without data while the data is being fetched, then show them when the data arrives.
You detect that you need to load in some data, so you set the UI to "waiting" mode, kick off the async fetch, then when the data comes in take it out of waiting mode. Perhaps by having the ViewModel subscribe to a "LoadCompleted" event on the object it's interested in.
(edit) You can avoid excessive loads or circular dependencies by keeping track of the state of each model object: Unloaded/Loading/Loaded.
Here is an option E for you.
Asynchronously load data. Have the initial fetch queue things up in a a background thread that fills out the full objects slowly. And make any methods that require data be loaded behind the scenes be blocking on the load finishing. (Blocking and have them notify the background thread that the data that they need is high priority, get them next, so you can unblock ASAP.)
This gives you a UI that is immediately responsive when it can be, the ability to write your code and not think about what has been loaded, and it will mostly just work. The one gotcha is that occasionally you'll make a blocking call while data is loading, however hopefully it won't do that too often. If you do, then in the worst case you degrade to something like option C where you have both a blocking fetch of data, and the ability to poll to see if it is there. However most of the time you wouldn't have to worry too much about it.
Disclaimer: I personally don't use Windows, and spend most of my time working on back ends far from UIs. If you like the idea, feel free to try it. But I haven't actually followed this strategy for anything more complicated than some behind the scenes AJAX calls in a dynamic web page.
精彩评论