WCF RIA Services 1.0 with ASP.Net 4.0 WebForms – some CRUD

I was trying to put together a demo to show the use of a shared WCF RIA Services Class Library with both a Silverlight client and an ASP.Net client. However, the only examples and documentation I could find about the ASP.Net side were essentially rehashes of the original RIA walk-through, in which a DomainDataSource control is dropped on a page, and then a GridView control is bound to it. This is nice for quick presentations, but what if you wanted to render something more useful like, say, a data entry form with live inserts and updates (and even deletes)?

This is what we’re after:

  ASP.Net: Silverlight:

image

image

Both platforms need to be able to accept an existing person to edit their registration, as well as insert a new registrant. I was able to find bits and pieces of the solution from about 5 sources, and so I wrote this post so that the information and solution will now all be in one place. Let’s dive into some code. Here’s what the project layout looks like:

image

CodeCamp2011.RIA.Date – the WCF RIA Services class library; there are plenty of resources out there to help you build one of these effectively, but I found the original MSDN walkthrough and Bryan Noyes series very helpful.
CodeCamp2011.RIA.UI – the Silverlight client – I used the JetPack Navigation project template (link can be found here).
CodeCamp2011.RIA.UI.Web – the ASP.Net client and host for the Silverlight client (the site also hosts the RIA Authentication bits).

I’m not going to spend much time in the Silverlight side except to say that I use the MVVM pattern (with the excellent Caliburn.Micro framework) for all of my current WPF, Silverlight and WP7 projects. For the Silverlight Save command in the Registration ViewModel, I simply used a basic RIA code approach:

public void Save()
{
    if (!string.IsNullOrEmpty(Password1) && !string.IsNullOrEmpty(Password2) && Password1 == Password2)
        Person.PasswordHash = Encryption.ComputePasswordHash(Password1);

    if (IsNewMode)
        context.Persons.Add(Person);
    else
    {
        if (Person.EntityState == EntityState.Detached) {context.Persons.Attach(Person);}
    }
    try
    {
        context.SubmitChanges();
        this.MessageBox.ShowMessage("Registration Saved", "Welcome to CodeCamp 2011!");       
    }
    catch(DomainOperationException ex)
    {
        this.MessageBox.ShowException(ex);
    }
    mode = Mode.Edit;
    Password1 = string.Empty;
    Password2 = string.Empty;
    IsDirty = false;
}

Person is one of my Entity classes, context is an instance of my RIA DomainContext and MessageBox is an instance of a StandardMessageBox class I wrote that implements an Interface and that shows a parameterized MessageBox. I did this so that the ViewModel is testable (via a separate constructor) without UI dependencies. For this demo, I did not use MEF to allow constructor injection of this interface type, and leave that as an exercise.

On the ASP.Net side, things appeared to be a bit iffy. We do not have direct access to the DomainService Context that we enjoy in Silverlight clients, but instead only have access to the DomainService instance or instances we have built. So that I would have one DomainService, I built separate DomainService class files for the various entities in my EF4 POCO model, marked them all as partial, and removed the [EnableClientAccess()] attribute from all but one of them (thanks, Colin Blair). The class library also has the EDMX Model in the its Web project so that I could generate the metadata classes,  and thus have access to all of the great RIA Navigation Properties (Includes) and Validation capabilities.

After some searching, I discovered that in order to send anything back to the server from an ASP.Net page, this DomainService needs to be initialized. Hmm, what does that mean? More research.

I found an example here that explained how to Initialize a DomainService from an ASP.Net MVC site, and it appeared to be (and, in fact, was) exactly what I was looking for. It used a static ServiceExtension class and an implementation of the required ServiceProvider for creating a DomainServiceContext in a Web site using the current HttpContext. Here are the two relevant classes that I slightly modified:

using System;
using System.IO;
using System.Security.Principal;
using System.ServiceModel.DomainServices.Server;
using System.Web;
using System.Web.Hosting;

namespace CodeCamp2011.RIA.UI.Web
{
    public static class ServiceExtensions
    {
        /// <summary>
        /// Initializes the domain service by creating a new <see cref="DomainServiceContext"/>
        /// and calling the base DomainService.Initialize(DomainServiceContext) method.
        /// </summary>
        /// <typeparam name="TService">The type of the service.</typeparam>
        /// <param name="service">The service.</param>        
        /// <param name="operationType">The Operation Type.</param>
        /// <returns></returns>
        public static TService Initialize<TService>(this TService service, DomainOperationType operationType)
            where TService : DomainService
        {
            DomainServiceContext context = null;
            switch (operationType)
            {
                case DomainOperationType.Invoke:
                    {
                        context = CreateDomainServiceContextForInvoke();
                        break;
                    }
                case DomainOperationType.Query:
                    {
                        context = CreateDomainServiceContextForQuery();
                        break;
                    }
                case DomainOperationType.Submit:
                    {
                        context = CreateDomainServiceContextForSubmit();
                        break;
                    }
            }
            service.Initialize(context);
            return service;
        }

        private static DomainServiceContext CreateDomainServiceContextForInvoke()
        {
            var provider = new ServiceProvider(new HttpContextWrapper(GetHttpContext()));
            return new DomainServiceContext(provider, DomainOperationType.Invoke);
        }
        private static DomainServiceContext CreateDomainServiceContextForSubmit()
        {
            var provider = new ServiceProvider(new HttpContextWrapper(GetHttpContext()));
            return new DomainServiceContext(provider, DomainOperationType.Submit);
        }

        private static DomainServiceContext CreateDomainServiceContextForQuery()
        {
            var provider = new ServiceProvider(new HttpContextWrapper(GetHttpContext()));
            return new DomainServiceContext(provider, DomainOperationType.Query);
        }

        private static HttpContext GetHttpContext()
        {
            var context = HttpContext.Current;
#if DEBUG
            // create a mock HttpContext to use during unit testing...
            if (context == null)
            {
                var writer = new StringWriter();
                var request = new SimpleWorkerRequest("/", "/", String.Empty, String.Empty, writer);
                context = new HttpContext(request){ User = new GenericPrincipal(new GenericIdentity("debug"), null) };
            }
#endif
            return context;
        }
    }
}

and

using System;
using System.Security.Principal;
using System.Web;

namespace CodeCamp2011.RIA.UI.Web
{
    public class ServiceProvider : IServiceProvider
    {
        private HttpContextWrapper wrapper;

        public ServiceProvider(HttpContextWrapper wrapper)
        {
            this.wrapper = wrapper;
        }

        private HttpContext _httpContext;
        public object GetService(Type serviceType)
        {
            if (serviceType == null)
            {
                throw new ArgumentNullException("serviceType");
            }
            if (serviceType == typeof(IPrincipal))
                return this._httpContext.User;
            if (serviceType == typeof(HttpContextBase))
                return this._httpContext;
            return null;
        }
    }
}

I wanted to allow the ServiceExtension’s Initialize method to support the Operation Types that I might use, so I added a DomainOperationType parameter. I placed these two classes in a Helpers folder to tidy my project up a bit. With that in place, here’s the code for Saving an existing or new Person:

protected void Save_Click(object sender, EventArgs e)
{
    if (IsValidPerson)
    {
        var person = IsNewMode ? new Person { Id = -1 } : Person;
            // reference Colin Blair (again) regarding not null ID property using RIA from ASP.Net for an Insert: 
            // http://forums.silverlight.net/forums/p/116668/266040.aspx

        person.Email = EmailAddress.Text;
        person.Name = UserName.Text;
        person.PasswordHash = Encryption.ComputePasswordHash(Password1.Text);
        person.Title = JobTitle.Text;
        person.Website = WebSite.Text;
        person.Twitter = Twitter.Text;
        person.Blog = Blog.Text;
        person.Bio = Bio.Text;

        // // Wrap it in ChangeSetEntry
        var operation = IsNewMode ? DomainOperation.Insert : DomainOperation.Update;
        var changeEntry = new ChangeSetEntry { Entity = person, Operation = operation };
        var changeSetEntries = new List<ChangeSetEntry> { changeEntry };
        var changeSet = new ChangeSet(changeSetEntries);
        // Send the changes back to the server as an array of ChangeSetEntries
        domainService.Initialize(DomainOperationType.Submit);
        domainService.Submit(changeSet);

        // Get the entity we just saved so we have the new ID if Inserted
        Person = domainService.GetPersonByEmail(Person.Email);

        // TODO: some kind of popup indicating success here
        Password1.Text = string.Empty;
        Password2.Text = string.Empty;

    }
}
I hope you find this article useful, and, of course, if you have any questions or comments, fire away using the link below. Hoped this helps.

Bob Baker

posted @ Thursday, January 6, 2011 5:21 PM

Print

 

Comments have been closed on this topic.