Tuesday, July 13, 2010

Data Driven Win Phone 7 Apps With MVC 2 JSON Services




I've been making a lot of little apps for Win Phone 7 in the past couple of weeks and I want to share a little bit of my code for other people who might be starting out and looking for some examples of creating a data driven Windows Phone 7 app.


We are going to be making a simple little product search for a fake company. We are going to show POSTing information to an MVC Controller action from our Win Phone 7 app, and we are going to show parsing a JSON response received from our MVC site and displaying it on the screen.


MVC 2 Website



The first thing I do when I'm creating a strictly service oriented site is to modify the Site.Master page to not display the login control, or the About page. I also, exclude the Account LogOn, Register and ChangePassword views so no one is tempted to register for the site. This step is optional, you can leave all of those things in, and you probably will have to if you have an existing MVC site you are altering.


Next, I create a new controller to serve my data. For our example we will call it ProductController and it will look something like this.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;

namespace WinPhone7.MVCExample.Website.Controllers
{
public class ProductInfo
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}

public interface IProductService
{
IQueryable<ProductInfo> GetProducts(Predicate<ProductInfo> filter);
void AddProduct(ProductInfo newProduct);
}

// This is dirty... but it's a demo
public static class ProductRepository
{
// An In-Memory persistable list of products.
public static List<ProductInfo> Products = new List<ProductInfo>()
{
new ProductInfo()
{
Id = 1,
Name = "Product 1",
Description = "Some sample product that is related to toys"
},
new ProductInfo()
{
Id = 2,
Name = "Product 2",
Description = "Some sample product that is related to sports"
},
new ProductInfo()
{
Id = 3,
Name = "Product 3",
Description = "Some sample product that is related to cars"
},
new ProductInfo()
{
Id = 4,
Name = "Product 4",
Description = "Some sample product that is related to boats"
},
new ProductInfo()
{
Id = 5,
Name = "Product 5",
Description = "Some sample product that is related to tools"
},
new ProductInfo()
{
Id = 6,
Name = "Product 6",
Description = "Some sample product that is related to technology"
},
};
}

public class MockProductService : IProductService
{
public IQueryable<ProductInfo> GetProducts(Predicate<ProductInfo> filter)
{
return ProductRepository.Products
.Where(prod => filter(prod))
.AsQueryable();
}

public void AddProduct(ProductInfo newProduct)
{
newProduct.Id = ProductRepository.Products.Count + 1;
ProductRepository.Products.Add(newProduct);
}
}

public class ProductSearchRequest
{
public string SearchTerm { get; set; }
}

public class ProductListResponse
{
public List<ProductInfo> Matches { get; set; }
}

public class ProductSearchResponse : ProductListResponse
{ }

public class ProductAddResponse
{
public bool Success { get; set; }
public int Id { get; set; }
}

public class ProductController : Controller
{
IProductService _products;

public ProductController()
: this(new MockProductService()) // Lazy man's dependency injection.
{

}

public ProductController(IProductService productServ)
{
_products = productServ;
}

public ActionResult All()
{
// Get all products by passing true predicate.
var allProducts = _products.GetProducts(prod => true);

var response = new ProductListResponse()
{
Matches = allProducts.ToList()
};

return new JsonResult()
{
// Make sure we allow get's
JsonRequestBehavior = JsonRequestBehavior.AllowGet,
// Set the data to our response.
Data = response
};
}

public ActionResult Search(ProductSearchRequest request)
{
var termLowered = request.SearchTerm.ToLower();
var matches = _products
.GetProducts(prod =>
prod.Name.ToLower().Contains(termLowered)
|| prod.Description.ToLower().Contains(termLowered));

var response = new ProductSearchResponse()
{
Matches = matches.ToList()
};

return new JsonResult()
{
JsonRequestBehavior = JsonRequestBehavior.AllowGet,
Data = response
};
}

[HttpPost]
public ActionResult Add(ProductInfo product)
{
ProductAddResponse response;
if (!String.IsNullOrEmpty(product.Name)
&& !String.IsNullOrEmpty(product.Description))
{
_products.AddProduct(product);

response = new ProductAddResponse()
{
Success = true,
Id = product.Id
};
}
else
response = new ProductAddResponse()
{
Success = false,
Id = -1
};

return new JsonResult()
{
Data = response
};
}
}


I've included the response and request classes in the code example for brevity, but I usually separate these out into files in the models folder. Once we have our products controller in place, we can go ahead and run the website and see some results. Hit F5 to run the MVC Website, then navigate to /Product/All to see your example JSON response (I highly recommend Google Chrome and this JSON Content Viewer, makes the JSON all color coded and nice).


Win Phone 7 Services



Next, I go ahead and create my Services project as a Win Phone 7 class library. Now, I've written some helper classes to deal with requesting web content and parsing JSON results. I've included it in the Sample Code as a library called Service4u2. The meat and potatoes of this library is the BaseJsonService class. This will save you tremendous amounts of work for consuming JSON services. Here is what a basic Product Search JSON Service looks like (I've included out specific app's URL Helper for brevity).



using System.Collections.Generic;
using Service4u2.Json;

namespace WinPhone7.MVCExample.Client.Services
{
// Extra stuff included for brevity.
public class ProductInfo
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}

public class ProductSearchRequest
{
public string SearchTerm { get; set; }
}

public class ProductListResponse
{
public List<ProductInfo> Matches { get; set; }
}

public class ProductSearchResponse : ProductListResponse
{ }

public class ProductAddResponse
{
public bool Success { get; set; }
public int Id { get; set; }
}

public static class ExampleUrlHelper
{
// Singleton pattern url helper.

private static ServiceUrlHelper _helper =
new ServiceUrlHelper("http://localhost.:49709");

public static ServiceUrlHelper Instance { get { return _helper; } }
}

// Product List Service.
public class ProductListService : BaseJsonService<ProductListResponse>
{
public void GetProductListAsync()
{
var url = ExampleUrlHelper
.Instance
.GetControllerActionParameterUrl("Product", "All", string.Empty);

StartServiceCall(url);
}
}

// Product Search Service.
public class ProductSearchService : BaseJsonService<ProductSearchResponse>
{
// We have to wrap our request so it is URL encoded properly for MVC.
// Our Product/Search action expects a parameter named request.
public class SearchRequestWrapper
{
public ProductSearchRequest request { get; set; }
}

public void SearchProductsAsync(ProductSearchRequest searchRequest)
{
var url = ExampleUrlHelper
.Instance
.GetControllerActionParameterUrl("Product", "Search", string.Empty);

var postData = new SearchRequestWrapper()
{
request = searchRequest
}.Postify(); // Postify our data, e.g. request.SearchTerm=blah&foo=1

StartServiceCall(url, HttpMethod.GET, postData);
}
}

// Product Add Service.
public class ProductAddService : BaseJsonService<ProductAddResponse>
{
// We have to wrap our request so it is URL encoded properly for MVC.
public class ProductAddWrapper
{
public ProductInfo product { get; set; }
}

public void AddProductAsync(ProductInfo productToAdd)
{
var url = ExampleUrlHelper
.Instance
.GetControllerActionParameterUrl("Product", "Add", string.Empty);

var postData = new ProductAddWrapper()
{
product = productToAdd
}.Postify();

StartServiceCall(url, HttpMethod.POST, postData);
}
}
}


Notice I've copied and pasted the request and response classes from the MVC project. This is a possible down-side to doing services this way. You won't get the autogenerated code that you get from a WCF service, but on the other hand, you will be better prepared in the long run to consume services from other websites that don't expose WCF implementations.


What we are doing is just preparing a URL and then making a request. The actual parsing of the JSON and converting it to our Result class is done by a DataContractJSONSerializer in the BaseJsonService class. To handle the result, or an error, just subscribe to the relevant events in your service consumer.


The Win Phone 7 App



The problem with the win phone 7 app example code is that everyone has different ways of doing things with WPF. I've put together a little MVVM framework tailored to the Win Phone 7 stuff and have included it with the example source code. I'll skip all the XAML and ViewModel code, because it's pretty standard MVVM stuff. Take a look at this demo video to get a look at how our app looks so far.




Hopefully, you've gotten pretty comfortable using MVC and JSON services with your Win Phone 7 apps. Feel free to use any of the code in this example. The Service4u2 and MVVM4u2 libraries are open source under the Microsoft Public License (Ms-PL).







Now Playing: Counting Crows - Friend of the Devil

1 comment:

  1. json is a very interesting language to be used. very good tutorial and can hopefully help me in building json in the application that I created for this lecture. thank you

    ReplyDelete