Saturday, April 24, 2010

Deep Dive into Custom RIA Service and Transactions

Silverlight Data and Controls Series

Introduction

In one of my previous blogs, I gave a quick introduction on MVVM, RIA Services and building a Custom Domain Service. Then in another blog I gave an introduction to the life-cycle of domain service methods. In this blog I am going to build upon them and have a deeper discussion on how to build up a custom RIA Domain Service with Data Transactions Objects (DTOs) that need to can connect to an existing data service layer which may not be something like Entity.

I had not intended for this blog to be as long as it is but it turned into a deeper dive than I expected. I found out some rather interesting things in the process. What I will discuss in depth is how to create associations between DTOs and how the ChangeSet is utilized with RIA Services.

DTOs

Here is the data model I created for this working example. In this example I am going to have an EmployeeDTO and a ProjectDTO. Every employee can have zero to many projects. As well a project can have many employees, so we will want to put an association table between the two called EmployeeProjectDTO. A typical many-to-many relationship.

Here are my three DTOs needed. As you can see the only relationships I have are the foreign key references however we can do better.

using System;
using System.ComponentModel.DataAnnotations;

namespace MVVM.Web.Model
{
public class EmployeeDTO
{
[Key]
public int ID { get; set; }

public string FirstName { get; set; }

public string LastName { get; set; }

public DateTime HireDate { get; set; }

public int Title { get; set; }
}
}

using System;
using System.ComponentModel.DataAnnotations;

namespace MVVM.Web.Model
{
public class ProjectDTO
{
[Key]
public int ID { get; set; }

public string Name { get; set; }

public DateTime StartDate { get; set; }

public int RegionalDirectorID { get; set; }
}
}

using System.ComponentModel.DataAnnotations;

namespace MVVM.Web.Model
{
public class EmployeeProjectDTO
{
[Key]
public int ID { get; set; }

public int EmployeeID { get; set; }

public int ProjectID { get; set; }

public bool IsProjectManager { get; set; }

public bool IsProgramDirector { get; set; }
}
}

Associating DTOs

Now that we have three DTOs, it is time to add some associations. What I am going to do is:

  • Add a list of EmployeeProjectDTO objects to EmployeeDTO to contain the list of projects an employee.
  • Add a list of EmployeeProjectDTO objects to ProjectDTO to contain a list of all the employees working on a project.
  • Add an EmployeeDTO property to ProjectDTO for the regional director.

Here are my DTOs again. Please take note of the new attributes that I have added to my properties that expose lists or complex types. Setting these up will ensure that objects will be returned and that when the data is submitted, insert, updates and deletes in the ChangeSet will be called in the correct order.

Please also take note that the foreign keys on all of the tables still remain. For example look at EmployeeDTO’s association to EmployeeProjectDTO. Notice that references “EmployeeID” and not “Employee.ID”. If you tried to use “Employee.ID” you will get an error saying “invalid: OtherKey property named 'Employee.ID' cannot be found on entity type”.

using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel.DataAnnotations;
using System.ServiceModel.DomainServices.Server;

namespace MVVM.Web.Model
{
public class EmployeeDTO
{
private List<EmployeeProjectDTO> _employeeProjects = new List<EmployeeProjectDTO>();

[Key]
public int ID { get; set; }

public string FirstName { get; set; }

public string LastName { get; set; }

public DateTime HireDate { get; set; }

public int Title { get; set; }

[Include]
[Composition]
[Association("EmployeeDTO_EmployeeProjectDTO", "ID", "EmployeeID")]
public List<EmployeeProjectDTO> EmployeeProjects {
get {
return _employeeProjects;
}
}
}
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel.DataAnnotations;
using System.ServiceModel.DomainServices.Server;

namespace MVVM.Web.Model
{
public class ProjectDTO
{
private List<EmployeeProjectDTO> _employeeProjects = new List<EmployeeProjectDTO>();
private EmployeeDTO _projectRegionalDirector;

[Key]
public int ID { get; set; }

public string Name { get; set; }

public DateTime StartDate { get; set; }

public int RegionalDirectorID { get; set; }

[Include]
[Association("ProjectDTO_RegionalDirectorEmployeeDTO", "RegionalDirectorID", "ID", IsForeignKey = true)]
public EmployeeDTO RegionalDirector {
get {
return _projectRegionalDirector;
}
set {
_projectRegionalDirector = value;
RegionalDirectorID = _projectRegionalDirector.ID;
}
}

[Include]
[Composition]
[Association("ProjectDTO_EmployeeProjectDTO", "ID", "ProjectID")]
public List<EmployeeProjectDTO> EmployeeProjects
{
get
{
return _employeeProjects;
}
}
}
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel.DataAnnotations;
using System.ServiceModel.DomainServices.Server;

namespace MVVM.Web.Model
{
public class EmployeeProjectDTO
{
private EmployeeDTO _employee;
private ProjectDTO _project;

[Key]
public int ID { get; set; }

public int EmployeeID { get; set; }

public int ProjectID { get; set; }

public bool IsProjectManager { get; set; }

public bool IsProgramDirector { get; set; }

[Include]
[Association("EmployeeProjectDTO_EmployeeDTO", "EmployeeID", "ID", IsForeignKey = true)]
public EmployeeDTO Employee
{
get {
return _employee;
}
set {
_employee = value;
if (_employee != null)
{
EmployeeID = _employee.ID;
}
else
{
EmployeeID = 0;
}
}
}

[Include]
[Association("EmployeeProjectDTO_ProjectDTO", "ProjectID", "ID", IsForeignKey = true)]
public ProjectDTO Project {
get
{
return _project;
}
set
{
_project = value;
if (_project != null)
{
ProjectID = _project.ID;
}
else
{
ProjectID = 0;
}
}
}
}
}

Getting Data

The following is a RIA Service with a method that will get all of the employees and fill it up with data. Please read through the data I set up because you will have to understand it for the following sections.

namespace MVVM.Web.Service
{
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.ServiceModel.DomainServices.Hosting;
using System.ServiceModel.DomainServices.Server;
using System.Security.Principal;

using Model;

[EnableClientAccess()]
[RequiresAuthentication()]
public class EmployeeDomainService : DomainService
{

public List<EmployeeDTO> GetEmployees()
{
List<EmployeeDTO> employees = new List<EmployeeDTO>();

//Create the employees
EmployeeDTO developer = new EmployeeDTO();
developer.ID = 1;
developer.FirstName = "Jason";
developer.LastName = "Apergis";
developer.Title = 1;
developer.HireDate = new DateTime(1990, 4, 15);
employees.Add(developer);

EmployeeDTO projectManager = new EmployeeDTO();
projectManager.ID = 2;
projectManager.FirstName = "Ethan";
projectManager.LastName = "Apergis";
projectManager.Title = 2;
projectManager.HireDate = new DateTime(2000, 6, 3);
employees.Add(projectManager);

EmployeeDTO programDirector = new EmployeeDTO();
programDirector.ID = 3;
programDirector.FirstName = "Caroline";
programDirector.LastName = "Apergis";
programDirector.Title = 3;
programDirector.HireDate = new DateTime(2005, 12, 20);
employees.Add(programDirector);

EmployeeDTO regionalDirector = new EmployeeDTO();
regionalDirector.ID = 4;
regionalDirector.FirstName = "Catherine";
regionalDirector.LastName = "Apergis";
regionalDirector.Title = 3;
regionalDirector.HireDate = new DateTime(2005, 12, 20);
employees.Add(regionalDirector);

//Create a project
ProjectDTO project = new ProjectDTO();
project.ID = 1;
project.Name = "Project ABC";
project.StartDate = new DateTime(2006, 6, 17);
project.RegionalDirector = regionalDirector;

//Create the employee to project associations
EmployeeProjectDTO employeeProject1 = new EmployeeProjectDTO();
employeeProject1.ID = 1;
employeeProject1.Project = project;
employeeProject1.Employee = developer;

EmployeeProjectDTO employeeProject2 = new EmployeeProjectDTO();
employeeProject2.ID = 2;
employeeProject2.Project = project;
employeeProject2.Employee = projectManager;
employeeProject2.IsProjectManager = true;

EmployeeProjectDTO employeeProject3 = new EmployeeProjectDTO();
employeeProject3.ID = 3;
employeeProject3.Project = project;
employeeProject3.Employee = programDirector;
employeeProject3.IsProgramDirector = true;

//Add the associations to the employee objects
developer.EmployeeProjects.Add(employeeProject1);
projectManager.EmployeeProjects.Add(employeeProject2);
programDirector.EmployeeProjects.Add(employeeProject3);

//Add the associations to the project object
project.EmployeeProjects.Add(employeeProject1);
project.EmployeeProjects.Add(employeeProject2);
project.EmployeeProjects.Add(employeeProject3);

return employees;
}

}
}

Persisting Data

To persist the data, I need to have the following methods on my custom domain service I just created. I have no code in here right now to actually wire up to the custom enterprise services I will be utilizing.

private static int count = 10;

public void InsertEmployeeDTO(EmployeeDTO employee) {
employee.ID = count++;
System.Diagnostics.Debug.WriteLine("Insert EmployeeDTO");
}

public void UpdateEmployeeDTO(EmployeeDTO employee)
{
System.Diagnostics.Debug.WriteLine("Update EmployeeDTO");
}

public void DeleteEmployeeDTO(EmployeeDTO employee)
{
System.Diagnostics.Debug.WriteLine("Delete EmployeeDTO");
}

public void InsertProjectDTO(ProjectDTO project)
{
project.ID = count++;
System.Diagnostics.Debug.WriteLine("Insert ProjectDTO");
}

public void UpdateProjectDTO(ProjectDTO project)
{
System.Diagnostics.Debug.WriteLine("Update ProjectDTO");
}

public void DeleteProjectDTO(ProjectDTO project)
{
System.Diagnostics.Debug.WriteLine("Delete ProjectDTO");
}

public void InsertEmployeeProjectDTO(EmployeeProjectDTO employeeProject)
{
employeeProject.ID = count++;
System.Diagnostics.Debug.WriteLine("Insert EmployeeProjectDTO");
}

public void UpdateEmployeeProjectDTO(EmployeeProjectDTO employeeProject)
{
System.Diagnostics.Debug.WriteLine("Update EmployeeProjectDTO");
}

public void DeleteEmployeeProjectDTO(EmployeeProjectDTO employeeProject)
{
System.Diagnostics.Debug.WriteLine("Delete EmployeeProjectDTO");
}

Scenario 1

Now I am ready to start using my RIA Services. I am not going to bind these objects to a Silverlight UI as I want to focus on the transactions. I am now going to give a couple scenarios and discuss which methods will be called in the RIA Services.

In the following code, demonstrates how I would promote the developer to a project manager of a specific project.

_context.Load<EmployeeDTO>(_context.GetEmployeesQuery());

//Update the developers EmployeeProjectDTO to IsProjectManager
EmployeeDTO developer = _context.EmployeeDTOs.
Where<EmployeeDTO>(e => e.ID == 1).FirstOrDefault();
EmployeeProjectDTO developerEmployeeProject = developer.EmployeeProjects.
Where<EmployeeProjectDTO>(ep => ep.ID == 1).FirstOrDefault();
developerEmployeeProject.IsProjectManager = true;

//Remove the previous Project Manager's association to the project
EmployeeDTO projectManager = _context.EmployeeDTOs.
Where<EmployeeDTO>(e => e.ID == 2).FirstOrDefault();
EmployeeProjectDTO projectManagerEmployeeProject = projectManager.EmployeeProjects.
Where<EmployeeProjectDTO>(ep => ep.Project.ID == developerEmployeeProject.Project.ID).FirstOrDefault();
projectManager.EmployeeProjects.Remove(projectManagerEmployeeProject);

//Save the changes
if (_context.HasChanges)
{
_context.SubmitChanges();
}

Based on the code above the following service methods will be called:

  • UpdateEmployeeDTO (Developer instance)
  • InsertEmployeeProjectDTO (Developer instance)
  • UpdateEmployeeDTO (Project Manager instance)
  • DeleteEmployeeProjectDTO (Project Manager instance)

One of the first things you may say is, well I did not modify anything on the EmployeeDTO objects, all I did was modify the EmployeeProjectDTOs. Well the great thing about RIA Services and the ChangeSet is that it recognizes a concept typically referred to as shallow and deep dirty. Even though the Employee DTOs were not directly modified, objects they were associated with are direct making them dirty (i.e. deep dirty). It is great that this is done because this allows you to potentially perform an operation on the EmployeeDTO if you need, like maybe update a time stamp or lock a record.

Scenario 2

In this scenario, let’s remove all of the employee projects from all the employees (i.e. the project has ended and the employees no longer need to be associated to the project however we do not want to delete the project itself).

_context.Load<EmployeeDTO>(_context.GetEmployeesQuery());

//Delete the project associations
foreach (EmployeeDTO employee in _context.EmployeeDTOs)
{
foreach (EmployeeProjectDTO employeeProject in employee.EmployeeProjects)
{
employee.EmployeeProjects.Remove(employeeProject);
}
}

//Save the changes
if (_context.HasChanges)
{
_context.SubmitChanges();
}

The following domain services methods will be called in the following order:

  • UpdateEmployeeDTO (Developer)
  • DeleteEmployeeProjectDTO (Developer)
  • UpdateEmployeeDTO (Project Manager)
  • DeleteEmployeeProjectDTO (Project Manager)
  • UpdateEmployeeDTO (Program Director)
  • DeleteEmployeeProjectDTO (Program Director)

Now you may be looking at the code above and say, if you want to remove all of the associations from a project what was just done is pretty error prone given I have no criteria to select which project has objects deleted. Plus, it would be more efficient to just remove the associations directly off the ProjectDTO. I agree. To do this, the first thing you will try to do is go _context.ProjectDTOs and quickly find out that the property does not exist. This is because there is no query methods yet on the domain service that returns a collection of ProjectDTOs. Even though in the GetEmployees() method I have initiated the objects to do what is need, it is not possible. All you need to do is add an empty method like the following to the domain service:

public List<ProjectDTO> GetProjects()
{
return new List<ProjectDTO>();
}

Now you will be able to write the following code:
_context.Load<EmployeeDTO>(_context.GetEmployeesQuery());

foreach (ProjectDTO project in _context.ProjectDTOs) {
foreach (EmployeeProjectDTO employeeProject in project.EmployeeProjects)
{
project.EmployeeProjects.Remove(employeeProject);
}
}

//Save the changes
if (_context.HasChanges)
{
_context.SubmitChanges();
}

The following domain services methods will be called in the following order:

  • UpdateProjectDTO (Project ABC)
  • DeleteEmployeeProjectDTO (Developer)
  • DeleteEmployeeProjectDTO (Project Manager)
  • DeleteEmployeeProjectDTO (Program Director)

This is still not the greatest example because I still called GetEmployees() to delete the projects. It would be better to call GetProjects() however I wanted to demonstrate a point that I have to a method on the RIA Services to return a collection of ProjectDTOs.

Scenario 3

Now in this scenario, let’s take the previous code and add to it code to create a brand new project and associate existing employees to that project.

_context.Load<EmployeeDTO>(_context.GetEmployeesQuery());

//Delete the project associations
foreach (ProjectDTO project in _context.ProjectDTOs) {
foreach (EmployeeProjectDTO employeeProject in project.EmployeeProjects)
{
project.EmployeeProjects.Remove(employeeProject);
}
}

//Get the employees
EmployeeDTO developer = _context.EmployeeDTOs.Where<EmployeeDTO>(e => e.ID == 1).FirstOrDefault();
EmployeeDTO projectManager = _context.EmployeeDTOs.Where<EmployeeDTO>(e => e.ID == 2).FirstOrDefault();
EmployeeDTO programDirector = _context.EmployeeDTOs.Where<EmployeeDTO>(e => e.ID == 3).FirstOrDefault();
EmployeeDTO regionalDirector = _context.EmployeeDTOs.Where<EmployeeDTO>(e => e.ID == 4).FirstOrDefault();

//Now let's add a new project
ProjectDTO newProject = new ProjectDTO();
newProject.Name = "New Project";
newProject.RegionalDirector = regionalDirector;

//Add the project to the context
_context.ProjectDTOs.Add(newProject);

//Create the EmployeeProjectDTO
EmployeeProjectDTO employeeProject1 = new EmployeeProjectDTO();
employeeProject1.Employee = developer;
employeeProject1.Project = newProject;

EmployeeProjectDTO employeeProject2 = new EmployeeProjectDTO();
employeeProject2.Employee = projectManager;
employeeProject2.Project = newProject;
employeeProject2.IsProjectManager = true;

EmployeeProjectDTO employeeProject3 = new EmployeeProjectDTO();
employeeProject3.Employee = programDirector;
employeeProject3.Project = newProject;
employeeProject3.IsProgramDirector = true;

//Create project associations
newProject.EmployeeProjects.Add(employeeProject1);
newProject.EmployeeProjects.Add(employeeProject2);
newProject.EmployeeProjects.Add(employeeProject3);

//Save the changes
if (_context.HasChanges)
{
_context.SubmitChanges();
}

The following domain services methods will be called in the following order:

  • InsertProjectDTO (New Project)
  • InsertEmployeeProjectDTO (Developer)
  • InsertEmployeeProjectDTO (Project Manager)
  • InsertEmployeeProjectDTO (Program Director)
  • UpdateProjectDTO (ABC Project)
  • DeleteEmployeeProjectDTO (Developer)
  • DeleteEmployeeProjectDTO (Project Manager)
  • DeleteEmployeeProjectDTO (Program Director)

One thing that you will notice when running this code is that some of the references to objects will be null. For instance in the InsertEmployeeProjectDTO calls, the EmployeeProjectDTO.Employee will be null but EmployeeProjectDTO.EmployeeID will not. This goes back to my earlier discussion on why we had to keep the primitive types for associations. Please note that even though EmployeeProjectDTO.Employee is null on the RIA Service Method call to InsertEmployeeProjectDTO, it is NOT null back on the code in the Silverlight project.

To test some things, I wanted to see if I could achieve the same thing but change some lines of code to instead create the new project from the employee perspective. You will notice I removed the line to add the ProjectDTO to the context and added the EmployeeProjectDTOs directly to the EmployeeDTOs.

_context.Load<EmployeeDTO>(_context.GetEmployeesQuery());

//Delete the project associations
foreach (ProjectDTO project in _context.ProjectDTOs) {
foreach (EmployeeProjectDTO employeeProject in project.EmployeeProjects)
{
project.EmployeeProjects.Remove(employeeProject);
}
}

//Get the employees
EmployeeDTO developer = _context.EmployeeDTOs.Where<EmployeeDTO>(e => e.ID == 1).FirstOrDefault();
EmployeeDTO projectManager = _context.EmployeeDTOs.Where<EmployeeDTO>(e => e.ID == 2).FirstOrDefault();
EmployeeDTO programDirector = _context.EmployeeDTOs.Where<EmployeeDTO>(e => e.ID == 3).FirstOrDefault();
EmployeeDTO regionalDirector = _context.EmployeeDTOs.Where<EmployeeDTO>(e => e.ID == 4).FirstOrDefault();

//Now let's add a new project
ProjectDTO newProject = new ProjectDTO();
newProject.Name = "New Project";
newProject.RegionalDirector = regionalDirector;

//Create the EmployeeProjectDTO
EmployeeProjectDTO employeeProject1 = new EmployeeProjectDTO();
employeeProject1.Employee = developer;
employeeProject1.Project = newProject;

EmployeeProjectDTO employeeProject2 = new EmployeeProjectDTO();
employeeProject2.Employee = projectManager;
employeeProject2.Project = newProject;
employeeProject2.IsProjectManager = true;

EmployeeProjectDTO employeeProject3 = new EmployeeProjectDTO();
employeeProject3.Employee = programDirector;
employeeProject3.Project = newProject;
employeeProject3.IsProgramDirector = true;

//Associate to the employee and manager
developer.EmployeeProjects.Add(employeeProject1);
projectManager.EmployeeProjects.Add(employeeProject2);
programDirector.EmployeeProjects.Add(employeeProject3);

//Save the changes
if (_context.HasChanges)
{
_context.SubmitChanges();
}

The following domain services methods will be called in the following order:

  • InsertProjectDTO (New Project)
  • UpdateEmployeeProjectDTO (Developer)
  • InsertEmployeeProjectDTO (Developer)
  • UpdateEmployeeProjectDTO (Project Manager)
  • InsertEmployeeProjectDTO (Project Manager)
  • UpdateEmployeeProjectDTO (Program Director)
  • InsertEmployeeProjectDTO (Program Director)
  • UpdateProjectDTO (ABC Project)
  • DeleteEmployeeProjectDTO (Developer)
  • DeleteEmployeeProjectDTO (Project Manager)
  • DeleteEmployeeProjectDTO (Program Director)

As you can see the exact same result was achieved.

Now if you have been paying attention you may be asking yourself, how come when I add the EmployeeProjectDTOs I do not add them to both the EmployeeDTOs and ProjectDTO like the following:

//Associate to the employee and manager
developer.EmployeeProjects.Add(employeeProject1);
projectManager.EmployeeProjects.Add(employeeProject2);
programDirector.EmployeeProjects.Add(employeeProject3);

//Create project associations
newProject.EmployeeProjects.Add(employeeProject1);
newProject.EmployeeProjects.Add(employeeProject2);
newProject.EmployeeProjects.Add(employeeProject3);

Well if I did this I would get the following error:

Submit operation failed. Invalid change-set : Entity for operation '0' has multiple parents.

The best I can figure is the ChangeSet just cannot handle this scenario because it does not want to potentially add the EmployeeProject objects more than once.

Alternate Approach

In some other posting I was reading when researching this topic I saw some examples where the following as presented as a solution. On the RIA Services instead of having the individual insert, update and delete methods for each object type, instead have an update method for the root object and then have methods like the following:

namespace MVVM.Web.Service
{
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.ServiceModel.DomainServices.Hosting;
using System.ServiceModel.DomainServices.Server;
using System.Security.Principal;

using Model;

[EnableClientAccess()]
[RequiresAuthentication()]
public class EmployeeDomainService : DomainService
{

public void UpdateEmployeeDTO(EmployeeDTO employee)
{
System.Diagnostics.Debug.WriteLine("Update EmployeeDTO");
SaveDTO(employee);
}

public void UpdateProjectDTO(ProjectDTO project)
{
System.Diagnostics.Debug.WriteLine("Update ProjectDTO");
SaveDTO(project);
}

private void SaveDTO(EmployeeDTO employee)
{
ChangeOperation employeeCO = this.ChangeSet.GetChangeOperation(employee);

switch (employeeCO)
{
case ChangeOperation.Insert:
//Add code to insert the EmployeeDTO
employee.ID = count++;
break;

case ChangeOperation.Update:
//Add code to update the EmployeeDTO
break;

case ChangeOperation.Delete:
//Add code to delete the EmployeeDTO
break;

case ChangeOperation.None:
break;
}

foreach (EmployeeProjectDTO employeeProject in this.ChangeSet.GetAssociatedChanges(employee, e => e.EmployeeProjects))
{
SaveDTO(employeeProject);
}
}

private void SaveDTO(ProjectDTO project)
{
ChangeOperation projectCO = this.ChangeSet.GetChangeOperation(project);

switch (projectCO)
{
case ChangeOperation.Insert:
//Add code to insert the EmployeeDTO
project.ID = count++;
break;

case ChangeOperation.Update:
//Add code to update the EmployeeDTO
break;

case ChangeOperation.Delete:
//Add code to delete the EmployeeDTO
break;

case ChangeOperation.None:
break;
}

foreach (EmployeeProjectDTO employeeProject in this.ChangeSet.GetAssociatedChanges(project, p => p.EmployeeProjects))
{
SaveDTO(employeeProject);
}
}

private void SaveDTO(EmployeeProjectDTO employeeProject)
{
ChangeOperation employeeProjectCO = this.ChangeSet.GetChangeOperation(employeeProject);
switch (employeeProjectCO)
{
case ChangeOperation.Insert:
//Add code to insert the EmployeeProjectDTO
employeeProject.ID = count++;
break;

case ChangeOperation.Update:
//Add code to update the EmployeeProjectDTO
break;

case ChangeOperation.Delete:
//Add code to delete the EmployeeProjectDTO
break;

case ChangeOperation.None:
break;
}
}

}
}

I have tested this and it would be a viable approach and support all of the examples that I presented earlier. I personally prefer the previous approach. Only drawback of this approach is you have to make sure you are always working with root objects and do not do this for non-root objects because it is possible that you can run a transaction twice and cause an error.

Thursday, April 22, 2010

Silverlight Localization and Resource Files

Silverlight Data and Controls Series

Using Resource files and doing localization in Silverlight is not much different than way we have traditionally done it in the past. Instead of writing a long drawn out blog; here are the blogs on MSDN that are really straight forward and get you up and running in no time.

Here are some other references in there you may find useful:

Wednesday, April 21, 2010

RIA Service Domain Service Method Life-Cycle

Silverlight Data and Controls Series

Introduction

In this blog I am going to give a short introduction into the methods that can be overwritten when implementing a RIA Service and discuss the execution life-cycle. You will need to get to know these methods if you are going to be writing custom RIA Services that may use existing services that already reside in the enterprise.

Below is some code from the Silverlight blog series I have been working on. I have overridden all of the methods and I am going to discuss the order and purpose of these methods.

namespace MVVM.Web.Service
{
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.ServiceModel.DomainServices.Hosting;
using System.ServiceModel.DomainServices.Server;
using System.Security.Principal;

using Model;

[EnableClientAccess()]
[RequiresAuthentication()]
public class EmployeeDomainService : DomainService
{
UserDTO _user;

public EmployeeDomainService(UserDTO user)
{
_user = user;
}

public override void Initialize(DomainServiceContext context)
{
//load up custom configurations
base.Initialize(context);
}

public override System.Collections.IEnumerable Query(QueryDescription queryDescription, out IEnumerable<ValidationResult> validationErrors, out int totalCount)
{
return base.Query(queryDescription, out validationErrors, out totalCount);
}

public List<EmployeeDTO> GetEmployees() {
List<EmployeeDTO> employees = new List<EmployeeDTO>();

EmployeeDTO employee = new EmployeeDTO();
employee.ID = 1;
employee.FirstName = "Jason";
employee.LastName = "Apergis";
employee.Title = 1;
employee.HireDate = new DateTime(1990, 4, 15);
employees.Add(employee);

employee = new EmployeeDTO();
employee.ID = 2;
employee.FirstName = "Ethan";
employee.LastName = "Apergis";
employee.Title = 2;
employee.HireDate = new DateTime(2000, 6, 3);
employees.Add(employee);

employee = new EmployeeDTO();
employee.ID = 3;
employee.FirstName = "Caroline";
employee.LastName = "Apergis";
employee.Title = 3;
employee.HireDate = new DateTime(2005, 12, 20);
employees.Add(employee);

return employees;
}

public List<TitleDTO> GetTitles() {
List<TitleDTO> titles = new List<TitleDTO>();

TitleDTO title = new TitleDTO();
title.ID = 1;
title.Title = "Developer";
titles.Add(title);

title = new TitleDTO();
title.ID = 2;
title.Title = "Manager";
titles.Add(title);

title = new TitleDTO();
title.ID = 3;
title.Title = "Senior Developer";
titles.Add(title);

title = new TitleDTO();
title.ID = 4;
title.Title = "Senior Manager";
titles.Add(title);

return titles;
}

public override bool Submit(ChangeSet changeSet)
{
return base.Submit(changeSet);
}

protected override bool AuthorizeChangeSet()
{
return base.AuthorizeChangeSet();
}

protected override bool ValidateChangeSet()
{
return base.ValidateChangeSet();
}

protected override bool ExecuteChangeSet()
{
return base.ExecuteChangeSet();
}

private static int count = 10;
public void InsertEmployeeDTO(EmployeeDTO employee) {
//Code to insert employee
employee.ID = count++;
}

public void UpdateEmployeeDTO(EmployeeDTO employee)
{
//Code to update employee
}

public void DeleteEmployeeDTO(EmployeeDTO employee)
{
//Code to delete employee
}

public override object Invoke(InvokeDescription invokeDescription, out IEnumerable<ValidationResult> validationErrors)
{
return base.Invoke(invokeDescription, out validationErrors);
}

protected override bool PersistChangeSet()
{
return base.PersistChangeSet();
}

protected override void OnError(DomainServiceErrorInfo errorInfo)
{
base.OnError(errorInfo);
}
}
}

Query RIA Service Method Life-Cycle

If you are going to run a Load or Invoke from the Silverlight like:

EmployeeDomainContext context = new EmployeeDomainContext();
context.Load<EmployeeDTO>(context.GetEmployeesQuery(), EmployeesLoaded, false);

The following RIA Service methods will be called in the following order:

  1. Constructor – I have constructor because I have implemented a Domain Service Factory which I discussed in this blog.
  2. Initialize – Initialize will always be called and is a good place to do an initialization or loading that would be needed.
  3. Query – This is a method that will be called before any of the query methods are called. This could be a good place to do some logging about the queries being made.
  4. GetEmployees or GetTitles – The query method itself will be called.

Submit RIA Service Method Life-Cycle

Next if the Silverlight application needed to run a Submit method like the following:

public void Save()
{
if (_context.HasChanges)
{
_context.SubmitChanges(new System.Action<System.ServiceModel.DomainServices.Client.SubmitOperation>(SubmitChangesCompleted), null);
}
}

private void SubmitChangesCompleted(System.ServiceModel.DomainServices.Client.SubmitOperation so)
{
RaisePropertyChanged("Employees");
}

The following RIA Service methods will be called in the following order:

  1. Constructor – I have constructor because I have implemented a Domain Service Factory which I discussed in this blog.
  2. Initialize – Initialize will always be called and is a good place to do an initialization or loading that would be needed.
  3. Submit – This is the method that is the starting point for the transaction. Like the Query method, this is a good place to do some logging about the Submit.
  4. AuthorizeChangeSet – This will be called next and will check to see if the user it authorized to submit. I really have not found much information on this method and do not have much reason to override it given I have implemented a custom authentication domain service. I guess you could potentially try to do some form of custom user validation here and if it fails, return a false value. This will stop the reset of the call stack from being executed.
  5. ValidateChangeSet – This next method will be called. All field validations and custom validation classes will be called subsequently called. You can do some logging here. As well if you want to put in logic to potentially fail the service call you can do that as well.
  6. ExecuteChangeSet – This method is the final method that will be called prior to the DTO Insert, Update and Delete methods. If you need to support a transaction, this is the recommended location on where to begin the transaction. It is commonplace to see developers try to begin the transaction in the Submit method but it would be best to wait and see if both authorization and validation succeed before beginning a transaction.
  7. InsertEmployeeDTO – Will be called for each object that has a state of insert insert in the ChangeSet. If there are none, it will not be called.
  8. UpdateEmployeeDTO – Will be called for each object that has a state of update insert in the ChangeSet. If there are none, it will not be called.
  9. DeleteEmployeeDTO – Will be called for each object that has a state of delete insert in the ChangeSet. If there are none, it will not be called.
  10. PersistChangeSet – This will be called after all of the insert, update and deletes have been run. This is where you would commit your transaction.

OnError Method Override

Note the OnError Method Override will be called anytime there is an unhandled error during the execution of the RIA service method call. The best thing you should do here is log the error and let the either the application gracefully handle the error.

References

http://weblogs.asp.net/fredriknormen/archive/2009/12/29/wcf-ria-services-domainservice-life-cycle-and-adding-transactions.aspx

Binding ComboBox and AutoComplete Controls in Silverlight DataGrid

Silverlight Data and Controls Series

Introduction

In this blog I am going to again build off a previous blog and dive a little bit deeper into binding with the MVVM pattern, Silverlight and RIA Services. In my first blog on this topic I gave a quick introduction into MVVM and how it can be used with textboxes and a grid. I subsequently wrote this blog which added a custom authentication domain service. I am going to build off the solution developed thus far to bind to more controls. Specifically I am going to discuss how binding DatePicker, ComboBox and AutoCompleteBox in and out of a DataGrid.

One thing I was very impressed with Silverlight of how easy it is to embed any control you want into a grid, including complete custom Silverlight user controls. The only tricky thing is getting some of the properties configured for ComboBox and AutoCompleteBox for Silverlight. It was a little tricky in my situation because I cannot leverage Entity to fill up my Data Transfer Objects (DTO).

Modifications to the Solution

Some classes that I had to Silverlight project were:

  • LookupProvider.cs – this is a class that is responsible for loading up lookup value collections.
  • TitleConvert.cs – this is a class that is used to convert values set in the AutoCompleteBox.
  • TitleDTO.cs – simple DTO class.
Untitled4Untitled5

EmployeeDTO Review

Let us first review the EmployeeDTO object. As you will see the Title is an int value loaded up from the database.

namespace MVVM.Web.Model
{
public class EmployeeDTO
{
[System.ComponentModel.DataAnnotations.Key]
public int ID { get; set; }

public string FirstName { get; set; }

public string LastName { get; set; }

public DateTime HireDate { get; set; }

public int Title { get; set; }
}
}

TitleDTO

Here is the TitleDTO, pretty straight forward.

namespace MVVM.Web.Model
{
public class TitleDTO
{
[System.ComponentModel.DataAnnotations.Key]
public int ID { get; set; }

public string Title { get; set; }
}
}

Employee Domain Service

Here is part of the code from the EmployeeDomainService. I have added a method called GetTitles to return a list of all the titles and I have modified GetEmployees so that it now has HireDate and Title values being returned.

    [EnableClientAccess()]
[RequiresAuthentication()]
public class EmployeeDomainService : DomainService
{
UserDTO _user;

public EmployeeDomainService(UserDTO user)
{
_user = user;
}

public override void Initialize(DomainServiceContext context)
{
//load up custom configurations
base.Initialize(context);
}

public List<EmployeeDTO> GetEmployees() {
List<EmployeeDTO> employees = new List<EmployeeDTO>();

EmployeeDTO employee = new EmployeeDTO();
employee.ID = 1;
employee.FirstName = "Jason";
employee.LastName = "Apergis";
employee.Title = 1;
employee.HireDate = new DateTime(1990, 4, 15);
employees.Add(employee);

employee = new EmployeeDTO();
employee.ID = 2;
employee.FirstName = "Ethan";
employee.LastName = "Apergis";
employee.Title = 2;
employee.HireDate = new DateTime(2000, 6, 3);
employees.Add(employee);

employee = new EmployeeDTO();
employee.ID = 3;
employee.FirstName = "Caroline";
employee.LastName = "Apergis";
employee.Title = 3;
employee.HireDate = new DateTime(2005, 12, 20);
employees.Add(employee);

return employees;
}

public List<TitleDTO> GetTitles() {
List<TitleDTO> titles = new List<TitleDTO>();

TitleDTO title = new TitleDTO();
title.ID = 1;
title.Title = "Developer";
titles.Add(title);

title = new TitleDTO();
title.ID = 2;
title.Title = "Manager";
titles.Add(title);

title = new TitleDTO();
title.ID = 3;
title.Title = "Senior Developer";
titles.Add(title);

title = new TitleDTO();
title.ID = 4;
title.Title = "Senior Manager";
titles.Add(title);

return titles;
}

LookupProvider

I next created a class that has the responsibility of loading lookup values. The code is pretty basic in that when the Titles property is referenced, it will retrieve the appropriate list of values.

using System.Collections.Generic;
using System.ComponentModel;
using System.ServiceModel.DomainServices.Client;
using System.Linq;
using MVVM.Web.Model;
using MVVM.Web.Service;

namespace MVVM.ViewModel
{
public class LookupProvider : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private EmployeeDomainContext _context;

public LookupProvider() {
_context = new EmployeeDomainContext();
}

public List<TitleDTO> Titles {
get {
if (_context.TitleDTOs.Count == 0)
{
_context.Load <TitleDTO>(_context.GetTitlesQuery(), TitlesLoaded, false);
}

return _context.TitleDTOs.ToList();
}
}

private void TitlesLoaded(LoadOperation<TitleDTO> lo)
{
RaisePropertyChanged("Titles");
}

private void RaisePropertyChanged(string propertyname)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyname));
}
}

}
}

TitleConverter

Next I created a class called TitleConverter which will be used for binding with the AutoCompleteBox. Note that it implements the IValueConverter interface. The Convert method take a numeric value passed in and retrieve the Title from the list. The ConvertBack does the opposite.

Note that if no values are found in the list, null is returned. This will actually happen a lot because these methods are called every time there is a keystroke in the control. Do not worry, it will not call out to the RIA service each time the user presses a key. It is just do a LINQ query against the list of Titles that is memory when the TitleConverter was first instantiated.

using System;
using System.Windows.Data;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using MVVM.Web.Model;

namespace MVVM.ViewModel
{
public class TitleConverter : IValueConverter
{
private LookupProvider _lookupProvider;

public TitleConverter() {
_lookupProvider = new LookupProvider();
}

public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {

if (value == null)
{
return null;
}

int i;

try
{
i = System.Convert.ToInt32(value);
}
catch (Exception ex)
{
return null;
}

IEnumerable<string> titles =
from t in _lookupProvider.Titles
where i == t.ID
select t.Title;

if (titles.Count() > 0)
{
return titles.FirstOrDefault();
}

return null;
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {

if (value == null)
{
return null;
}

string title;

try
{
title = System.Convert.ToString(value);
}
catch (Exception ex)
{
return null;
}

IEnumerable<int> titles =
from t in _lookupProvider.Titles
where title == t.Title
select t.ID;

if (titles.Count() > 0)
{
return titles.FirstOrDefault();
}

return null;
}
}
}

Binding to User Controls Outside of Grid

So first let’s look to see how things are bound outside of the grid. As I mentioned, I added to my user control a DatePicker, ComboBox and AutoCompleteBox.

The DatePicker is really straight forward; all we need to do is bind on the SelectedDate property in the same way we have bound other controls.

Next the ComboBox needs to be configured. We need to show the list of titles in the dropdown and bind it to the Title property of the current EmployeeDTO object that is in the EmployeeViewModel object instance. To set the values in the dropdown, we need to first set the ItemSource attribute. The ItemSource is bound to the Titles property of the LookupProvider which returns a List<TitleDTO>. Notice there is a UserControl.Resources tag at the top which creates an instance of the LookupProvider. Then both the DisplayMemberPath and SelectedValuePath are set to the appropriate properties of the TitleDTO class. Finally the Combo Box has the SelectedValue attribute which needs to be set to the Title property of the EmployeeDTO class. Now when a Title is selected in the dropdown the integer id for the TitleDTO will be set to the EmployeeDTO object.

The AutoCompleteBox is similar but there are a few differences in the configuration. The ItemSources attribute is again set to Titles property of the LookupProvider. Next the ValueMemberBinding is set to Title of the TitleDTO. As well the TextBlock is bound to the Title of the TitleDTO (the TextBlock is the control that is displayed when the user has not clicked into the AutoCompleteBox). Now we need to bind the actual Employee object to the Text attribute. Notice the Converter is set to StaticResrouce titleConverter. This is where we have to use the TitleConverter class that I created earlier. I had to also add a UserControl.Resources tag to create an instance of the TitleConverter.

<UserControl x:Class="MVVM.View.UpdateView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mvvm="clr-namespace:MVVM.ViewModel"
mc:Ignorable="d"
d:DesignHeight="79" d:DesignWidth="642" xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk">

<UserControl.Resources>
<mvvm:LookupProvider x:Name="lookupProvider" />
<mvvm:TitleConverter x:Key="titleConverter" />
</UserControl.Resources>

<Grid x:Name="LayoutRoot" Background="White">
<TextBox Height="23" HorizontalAlignment="Left" Margin="12,12,0,0"
Name="txtFirstName" VerticalAlignment="Top" Width="120"
Text="{Binding SelectedEmployee.FirstName, Mode=TwoWay}"/>
<TextBox Height="23" HorizontalAlignment="Left" Margin="138,12,0,0"
Name="txtLastName" VerticalAlignment="Top" Width="120"
Text="{Binding SelectedEmployee.LastName, Mode=TwoWay}"/>
<Button Content="Save" Height="23" HorizontalAlignment="Left"
Margin="559,46,0,0" Name="btnSave" VerticalAlignment="Top"
Width="75" Command="{Binding SaveCommand}"/>
<sdk:DatePicker Height="23" HorizontalAlignment="Left"
Margin="262,12,0,0" Name="dtpHireDate"
VerticalAlignment="Top" Width="120"
SelectedDate="{Binding SelectedEmployee.HireDate, Mode=TwoWay}"/>
<ComboBox Height="23" HorizontalAlignment="Left" Margin="388,12,0,0"
Name="cboTitle" VerticalAlignment="Top" Width="120"
ItemsSource="{Binding Titles, Source={StaticResource lookupProvider}}"
DisplayMemberPath="Title"
SelectedValuePath="ID"
SelectedValue="{Binding SelectedEmployee.Title, Mode=TwoWay}"/>
<sdk:AutoCompleteBox Height="28" HorizontalAlignment="Left"
Margin="514,12,0,0" Name="acbTitle"
VerticalAlignment="Top" Width="120"
ItemsSource="{Binding Titles, Source={StaticResource lookupProvider}}"
ValueMemberBinding="{Binding Title}"
Text="{Binding SelectedEmployee.Title, Mode=TwoWay, Converter={StaticResource titleConverter}}">
<sdk:AutoCompleteBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</sdk:AutoCompleteBox.ItemTemplate>
</sdk:AutoCompleteBox>
</Grid>
</UserControl>

Binding to User Controls in a Grid

The last part of this blog I am going to discuss is how to set up these same controls in a grid. First I set the AutoGenerateColumns attribute of the grid to false. Then I have to start manually adding columns into the <sdk:DataGrid.Columns>. Notice setting up textboxes is really straight forward.

Next I need to set up the DatePicker control. I first add a DataGridTemplateColumn which will allow me to basically add any control that I want to into a grid column. In this case I put in a DataPicker and the binding of that column is identical to above.

Next I need to put in the ComboBox and the AutoCompleteBox in the same manner reusing the same xaml code I used above.

<UserControl x:Class="MVVM.View.EmployeeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mvvm="clr-namespace:MVVM.ViewModel"
mc:Ignorable="d"
d:DesignHeight="210" d:DesignWidth="645" xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk">

<UserControl.Resources>
<mvvm:LookupProvider x:Name="lookupProvider" />
<mvvm:TitleConverter x:Key="titleConverter" />
</UserControl.Resources>

<Grid x:Name="LayoutRoot" Background="White">
<sdk:DataGrid AutoGenerateColumns="False" Name="grdEmployees"
Height="168" VerticalAlignment="Top"
HorizontalAlignment="Left" Width="645"
ItemsSource="{Binding Employees}"
SelectedItem="{Binding SelectedEmployee, Mode=TwoWay}">
<sdk:DataGrid.Columns>
<sdk:DataGridTextColumn
Header="ID" Binding="{Binding ID}" />
<sdk:DataGridTextColumn
Header="First Name" Binding="{Binding FirstName}" />
<sdk:DataGridTextColumn
Header="Last Name" Binding="{Binding LastName}" />
<sdk:DataGridTemplateColumn Header="Hire Date">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<sdk:DatePicker
SelectedDate="{Binding HireDate, Mode=TwoWay}" />
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
</sdk:DataGridTemplateColumn>
<sdk:DataGridTemplateColumn Header="Title">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Titles, Source={StaticResource lookupProvider}}"
DisplayMemberPath="Title"
SelectedValuePath="ID"
SelectedValue="{Binding Title, Mode=TwoWay}"/>
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
</sdk:DataGridTemplateColumn>
<sdk:DataGridTemplateColumn Header="Auto Title">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<sdk:AutoCompleteBox Name="acbTitle"
IsTextCompletionEnabled="True"
ItemsSource="{Binding Titles, Source={StaticResource lookupProvider}}"
ValueMemberBinding="{Binding Title}"
Text="{Binding Title, Mode=TwoWay, Converter={StaticResource titleConverter}}">
<sdk:AutoCompleteBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</sdk:AutoCompleteBox.ItemTemplate>
</sdk:AutoCompleteBox>
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
</sdk:DataGridTemplateColumn>
</sdk:DataGrid.Columns>
</sdk:DataGrid>
<Button Content="Insert" Height="23" HorizontalAlignment="Left" Margin="479,175,0,0"
Name="btnInsert" VerticalAlignment="Top" Width="75"
Command="{Binding InsertCommand}"/>
<Button Content="Delete" Height="23" HorizontalAlignment="Left" Margin="560,175,0,0"
Name="btnDelete" VerticalAlignment="Top" Width="75"
Command="{Binding DeleteCommand}"/>
</Grid>
</UserControl>

Gird Configuration Alternative

There is one small alternative you can do with the display of controls in a grid. If the user has no clicked into a cell, you may only want to show a text control but when they click into the cell to edit the value, show them the appropriate control. For instance you may want to only show the DatePicker or ComboxBox when the user has clicked in to save on real estate. Only downside of this approach is the user may have to do a little more clicking.

Here is how you would do this for the DatePicker column that I showed you above. Here you will see there is a CellTemplate and a CellEditingTemplate.

<sdk:DataGridTemplateColumn Header="Hire Date">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding HireDate}" />
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
<sdk:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<sdk:DatePicker SelectedDate="{Binding HireDate, Mode=TwoWay}" />
</DataTemplate>
</sdk:DataGridTemplateColumn.CellEditingTemplate>
</sdk:DataGridTemplateColumn>