Aren’t we all tired of the consultant weasel-words “it depends”? Every once in awhile it is possible to speak in unequivocal and unambiguous language. This is one of those moments.
A ViewModel should never reference the persistence machinery directly; it should always delegate the getting and saving of Model objects to a helper such as a DataService (aka Repository, aka ServiceAgent).
Are there exceptions? I can’t think of a single one. I wouldn’t break this rule in an MVVM demo.
Now if I’m not using the MVVM pattern, fine. I just mandated a rule for ViewModel, not a rule for all UI architectures.
This really is one of the laws of ViewModel construction, taking a privileged place besides a very few other such laws as “The ViewModel may not have a reference to the View” and “The ViewModel may not refer to a type that requires a running view to construct.”
If you break these laws, you are not using MVVM. You’re doing something else. And I’m pretty sure you’re going to suffer for it.
You won’t suffer because the MVVM Police will get you. You’ll suffer because you added MVVM complexity to your application for nothing. You paid the price for a UI separation pattern … and destroyed that separation. You now have an application that is harder to read and maintain than if you’d put everything in code behind.
Who Put The Hot Sauce in Grandpa’s Oatmeal?
I’ll give you backstory and then I’ll repeat here what I said on the WCF RIA Services forum.
I frequently troll the WCF RIA Services forum to find out what RIA developers are thinking and <grin> to learn of the many ways in which IdeaBlade’s DevForce is superior.</grin>
I’m especially fond of Colin Blair’s contributions. Colin knows more about RIA Services than almost anyone; sometimes I think he knows more than the product team. But I especially like Colin’s posts because he always goes beyond the “how to”. He’s deeply interested in understanding what you are trying to do and in helping you move in a direction that gets you where you want to be. And I probably like his stuff because I agree with it. Except this time.
This time he gave bad advice.
A developer identified as “lein4d” posted the following question (amended for grammar and style, highlights added)
When using MVVM Pattern with WCF RIA Services, is it better to encapsulate the client side RIA code in service classes than to have it directly inside the ViewModel?
In many of the recent MVVM examples I've seen, the classes are separated into ViewModel, IServiceAgent, and ServiceAgent classes, in which only the ServiceAgent Class knows about RIA details.
I've been following this approach for a while, but I found it too cumbersome to create all the boilerplate code each time I create another ViewModel.[???] The application I'm creating doesn't have unit tests, and is not making use of design-time data.[!!!]
In this case, is it still necessary to abstract away the RIA service detail? I think it'll be more productive and write less duplicate code if I have DomainContext class directly inside ViewModel and load entities from there.
What is the rationale behind having IServiceAgent and ServiceAgent classes? As for sharing queries between ViewModels, isn't it sufficient to put shared queries in the server side?
This is a great question for which there is a definitive answer. Unfortunately, Colin gave him the wrong answer. He told lein4d that it is ok to call the DomainContext directly in the ViewModel. Kyle McClellan of the RIA Services team added “Feel free to use the DomainContext directly in your ViewModel.”
It is never ok to call the DomainContext (or the DevForce EntityManager) within a ViewModel. You should not be “feeling free” when you do that.
Although I am shy and generally unwilling to express myself forcefully, I found myself writing this spirited reply.
Why ViewModel Should Delegate Persistence To A Service
@Lein4d, why are you using ViewModel at all? Why not put everything in the code behind? It's a lot easier and there are fewer classes to worry about. The primary benefits of ViewModel are single responsibility, design/development independence, and testability. You don't seem to be interested in any of them … in which case the MVVM pattern isn't pulling its weight.
I don't have a lot of hard and fast rules. I do have a few. This is one of them.
"No persistence machinery in the ViewModel"
I'm going to try to persuade you of this even if you don't seem to care if your ViewModels are readable (sigh), don't test your ViewModels (sigh), and don't design with data (sigh).
I'm going to make my case solely on the Single Responsibility Principle and its implications for consistency and maintainability. If these characteristics don't matter to you ... stop reading ... and stop bothering with MVVM or any other pattern.
I ask all of my customers to write ViewModels that concentrate on providing state (data) and behavior (logic) that their views need. Complex tasks that are not strictly concerned with the view (e.g., how to query for entities or save them) should be delegated to supporting "services". A focused ViewModel is easier to understand and easier to test [oops ... I mentioned those points again]. It is also less vulnerable to changes that have nothing to do with the View.
The DomainContext itself is already a step along this road. Without it, in the extreme case, your ViewModel would be making its own connections to the server, issuing web requests on a background thread, waiting for the response, interpreting the response, mapping received data into entities and marshalling them back to the UI thread. You wouldn't dream of writing that code in each of your Views or ViewModels. The DC abstracts that for you.
But it's not a sufficient abstraction for the ViewModel. There are still too many mechanics. Let's get concrete.
Suppose the ViewModel provides Customers to the View. That's really all the VM cares about ... some notion of a list of Customers. It wants to have a method like "GetCustomers". Why should it care about how that method is implemented?
Should that method return all customers or only active customers? Only customers that the current user is allowed to see? Can it be customers in cache or should it be customers fresh from the database? Should the query be for customers only or should the query include related entities as well? Should the customers be sorted? Is there an upper bound on the number of Customers to retrieve for this view? Should the query use an existing DomainContext or create a new one? What if the user isn't authenticated? Or not authorized? What if the DC can't connect to the server? What if the server returns an error?
These are all important considerations. None of them have anything to do with getting customers onto the screen ... which is the VM's job.
Will you worry about all of these considerations on day one? Probably not. But when they do become important to you, would you expect to edit all of your ViewModels to address them? Or would you expect to edit a Data Service to address them?
For me the answer turns on this: would changing the way I get the customers change the appearance of the view or how the user interacts with the view? If the answer is "no", then the code belongs in a Data Service, not the ViewModel.
Let's generalize that thought. You should be able to answer the following question for all code in your ViewModel:
Would changing this code alter the appearance of the view or how the user interacts with the view? If "yes", the code can stay. If "no", the code belongs elsewhere, perhaps in a helper class to which the VM delegates.
This is not a hypothetical issue or an academic discussion. You asked for advice rooted in experience. Presumably if you were certain, you wouldn't ask. So listen up to someone who has been working in this corner of .NET since 2003.
I've looked at a lot of customer code. That's part of my job description: work with customers who are building smart client / RIA applications. I’m kind of a code doctor. People call me when their code is sick.
Obviously I wasn't always looking at Silverlight code; usually it was Windows Forms where the corresponding collaborator is the "Presenter" in an MVP pattern. Every time I saw persistence code in the VM or Presenter, I saw trouble. Every one of those VMs or Presenters implemented their queries differently. Quite often the same conceptual query would appear in multiple guises. I'd see three or four variations on GetCustomers, all of them unintentionally different.
Why those differences? Maybe the developers understood the business rules differently. Maybe the rules changed as the application evolved. Maybe some of the developers (or the same developer at different times) didn't understand how the persistence machinery works when they wrote the code. Maybe awareness of potential errors was acute on some days and not others.
Whatever the cause, it's a mess ... a mess spread across your code base. Get that stuff out of there. Put it in a Data Service where you can find it, watch it, and tune it.
P.S.: If you ever decide to test your ViewModels and/or test the many ways that your application accesses data, you'll be glad you made this move.
A final detail. Your question seemed to imply that you were creating a new "ServiceAgent" and interface for each ViewModel. You wrote "I found it too cumbersome to create all these boilerplate codes each time I create another ViewModel."
I would find that too cumbersome as well. I tend to write one "ServiceAgent" (DataService, Repository, whatever) per module. That's not a strict rule. But I do find that I use the same service across multiple VMs. John Papa's tiny BookClub example has a single "BookService" that does the trick for both the BookViewModel and the CheckoutViewModel. You really don't need a lot of these services in your application.
Colin Replies
After I posted, Colin and I had a spirited exchange which you can read on the forum. I won’t repeat it all here. But I will summarize his objections and my counter arguments.
#1 “The primary purpose of the ViewModel is data binding”.
No it isn’t. The powerful data binding in the XAML platforms made MVVM feasible. Just because we can do it, doesn’t mean we should. I favor MVVM because it encourages three good things: Single Responsibility, Designer / Developer independence, and automated testing.
#2 [Paraphrasing] You don’t need a data service for a “Hello, World” application.
True enough. You don’t need MVVM either. More to the point, you probably shouldn’t be using MVVM in a “Hello, World” application.
#3 [Partial quotation? Misquotation? My interpretation] “I’m uncomfortable telling someone that they are a bad programmer because [they don’t do what I think they should do]”.
I didn’t call anyone a bad programmer. Lein4d asked if he should use a Data Service in his real world MVVM-base application. I didn’t say “maybe”. I said emphatically “yes”. We should be comfortable in our convictions and uncomfortable when we waffle.
I try to come clean when I’m unsure. Often the answer depends upon circumstances. Often I have opinions and experience but nothing I’d want to mandate and plenty of counter examples.
On this particular and rare occasion, I harbor no such doubts.
Conclusion
If you are going to adopt the MVVM pattern, realize that it adds complexity for a purpose: to improve your application’s intelligibility, maintainability, testability, and designability (ugh). Don’t pay the complexity cost and throw the benefits in the trash. Please delegate all persistence concerns to a helper service.
You’ll be glad you did.
9 comments:
Well put, Ward. I'm trying to convince a client of the same thing and struggling to get them on board. Hopefully your comments are just the ammunition I need to make the point.
--Jeff
Its good to know the world is still black and white. I'm sure colin was momentarily confused when he blathered "just use the domain context". I'm sure he didn't mean it.
There is a difference between having your persistance mechanism separated from your ViewModel and having a particular implementation of that separation. My logic was based on not wanting to mandate a particular implementation but I had managed to let that thinking corrupt itself into not wanting to mandate having any separation at all. It was bad advice and the referenced thread has been corrected.
@jeff - every little bit helps. Have your client follow the thread back to the RIA Forum (http://forums.silverlight.net/forums/p/217498/517690.aspx#517690) where there is plenty of amplifying commentary.
@peter - I prefer black&white to beige but what do I know? Maybe developers should hire interior decorators. :)
@Colin - All is forgiven. Come home.
How refreshing to hear a firm opinion - and, FWIW, one that I fully endorse.
Well said.
Nice post on MVVM!
Our recent projects use the ModelServices and Repository pattern, which is completely separated from the ViewModel. The benefit is obvious; this allows us to change the data access strategy without any modification to the Views and ViewModels.
We also shipped a new Grid (For SL & WPF) that works with DevForce 6.0.8 in loosely-coupled design. Anyone interested can head to http://wp.me/p1S6Z-xn (DF sample projects included!).
Jimmy - Right you are! "Repository" is another name for "Data Service".
Others can learn about a sample that combines DevForce and Intersoft technologies here.
Ward,
You talk about using a ViewModelLocator to decide whether your ViewModel gets a design friendly or live Repository/ServiceAgent/Gateway. To me that alone justifies ALWAYS keeping enviroment dependant code out of the ViewModel, hence violent head nodding with this post. Since your annoying nephew won't let us party on Blend SampleData forever, what does a design friendly Repository look like? Mocked? Dummy database? Some crazy gateway to local static resources? I'm sure in different scenarios they all might be the right answer but usually there is one default answer a developer holds onto until the scenario requires otherwise. What is your default answer for the implementation of a design time repository?
Matt Poland
@Matt - Sorry for delay in publishing your comment. Spam forces me to moderate comments and I don't get to them as fast I'd like.
I've had success with a single Repository/DataService class. It's ctor takes an IEntityManagerProvider (or IEntityManagerFactory) parameter which, in turn, delivers an appropriate DevForce EntityManager (think DomainContext if you come from RIA Services).
In a design context, that EntityManager will be offline and populated with design data. The design data could be entities that are "newed" up or read from a serialized XML file.
You'll find both approaches in the DevForce version of BookShelf.
I supposed that leads to the question "Who decides whether to use the runtime EntityManagerProvider of the design-time provider?" I do that in the application start up logic (Bootstrapper). Clearly an IoC would be appropriate here; I didn't use IoC for fear of scaring off a valued portion of my audience.
HTH
Post a Comment