While I was writing the BBQ Shack, I noticed that view model code calling into the business layer was repetitive across view models. The pattern I used was simple and widely accepted; background worker to make the asynchronous calls to the business layer.
The below code snippet was typical. Background worker, exception checking, call to business layer, successful result code path, and exception result code path.
Old Pattern
void BackgroundWorker_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e) {
e.Result = _iBLLItemCategory.Select(Convert.ToInt32(e.Argument));
}
void BackgroundWorker_RunWorkerCompleted(object sender,
System.ComponentModel.RunWorkerCompletedEventArgs e) {
if (e.Error == null) {
if (e.Result != null) {
this.ic_ItemCategory = (ic_ItemCategory)e.Result;
} else {
_iViewModelUIService.MessageDialog("Data Error",
"Record Not Found",
string.Format("Item Category key: {0} not found", _objLoadKey.ToString));
}
} else {
_objIViewModelUIService.ExceptionDialog(e.Error);
}
}
I kept thinking about this, always looking for a better pattern. I also wanted a pattern that only required a single line of code and still gave me all of the same benefits of the above code.
New Pattern
All of the above code condenses nicely down to one line of code that provides all of the above “old pattern” features.
The below repository method signatures describe the pattern. The pattern for the method signatures is zero or more parameters, followed by the same last two; Action<T> resultCallback
and Action<Exception> errorCallback
.
public interface IItemRepository {
void GetAll(Action<IEnumerable<Item>> resultCallback, Action<Exception> errorCallback);
void Get(Int32 itemId, Action<Item> resultCallback, Action<Exception> errorCallback);
Item Create();
}
The successful result of the method call always calls the resultCallback
delegate.
The unsuccessful result of the method call always calls the errorCallback
delegate.
This pattern is incredibly simple and leads to much cleaner code in the view model. The errorCallback
is a method in the view model base class. This method can use a variety of techniques for displaying the error message to the user.
void LoadItems() {
_itemRepository.GetAll(result => { this.DataItems.Source = result; }, this.DisplayException);
}
Repository Implementation – Bring on the Task Parallel Library (TPL)
The below implementation leverages the TPL Futures pattern. Use of the TPL is optional, I’m using it here in this WPF application. The TPL is not available in Silverlight 4 applications yet. For Silverlight applications, change the implementation to use the common Silverlight asynchronous pattern.
The “pattern” is described in the above IItemRepository
interface. Like all interface contracts, implementation specifics can be platform specific and up to the developer.
I strongly recommend you read this book on MSDN: Parallel Programming with Microsoft .NET. The book can also be purchased from Amazon here. There is a book and Kindle version available.
[Export(typeof(IItemRepository))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class ItemRepository : IItemRepository {
readonly ThePhoneCompanyEntities _dataService;
[ImportingConstructor]
public ItemRepository(DataServiceFacade dataServiceFacade) {
_dataService = dataServiceFacade.DataService;
}
void IItemRepository.GetAll(Action<IEnumerable<Item>> resultCallback,
Action<Exception> errorCallback) {
Task<RepositoryResult<IEnumerable<Item>>> task =
Task.Factory.StartNew(() => {
try {
return new RepositoryResult<IEnumerable<Item>>(
_dataService.Items.ToList(), null);
} catch(Exception ex) {
return new RepositoryResult<IEnumerable<Item>>(null, ex);
}
});
task.ContinueWith(r => {
if(r.Result.Error != null) {
errorCallback(r.Result.Error);
} else {
resultCallback(r.Result.Package);
}
}, CancellationToken.None, TaskContinuationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
}
void IItemRepository.Get(Int32 itemId, Action<Item> resultCallback,
Action<Exception> errorCallback) {
if(itemId != 0) {
Task<RepositoryResult<Item>> task =
Task.Factory.StartNew(() => {
try {
return new RepositoryResult<Item>(
_dataService.Items.Where(
i => i.ItemID == itemId).FirstOrDefault(), null);
} catch(Exception ex) {
return new RepositoryResult<Item>(null, ex);
}
});
task.ContinueWith(r => {
if(r.Result.Error != null) {
errorCallback(r.Result.Error);
} else {
resultCallback(r.Result.Package);
}
}, CancellationToken.None, TaskContinuationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
} else {
resultCallback(((IItemRepository)this).Create());
}
}
Item IItemRepository.Create() {
return new Item();
}
}
Key Points
- Call that does the actual work is wrapped in a
try catch
block.
- The result of the asynchronous operation is wrapped in a
RepositoryResult
object.
- The
RepositoryResult
is used to drive invoking the resultCallback
or the errorCallback
on the calling thread. The TaskScheduler.FromCurrentSynchronizatikonContext
method call in the task. ContinueWith
ensures the two callbacks are invoked on the calling thread.
Download
This code is from one of my presentations at the patterns & practices 2010 Symposium.
The article and video is here: http://blogs.msdn.com/b/kashiffl/archive/2010/10/21/patterns-and-practices-2010-symposium-content.aspx
This code can be downloaded from the article.
Close
I hope that this short post encourages you to look for possible refactorings in your own code and to take a look at the TPL.
Have a great day,
Just a grain of sand on the worlds beaches.