Tuesday, May 15, 2012

Learning MVC: Updating the Many-to-many Relationship with MultiSelectList

Continuing the application from the last post, I was now going to use a MultiSelectList to update the many-to-many relationship. The use case is the following: suppose we have a recipe but want to update it - maybe the dish will benefit from adding a bit of pepper. So how do I go about adding something to the list of ingredients? The solution was not so straightforward and there were at least three "gotchas" on the way.

Gotcha 1. To actually populate the MultiSelectList. The easy (but not quite straightforward) task was to create the MultiSelectList and to populate it with all the ingredients. To do that I added the properties to the Recipe class called AllIngredients and SelectedIngredientIDs. The most basic Recipe class now looks like this:

public class Recipe
{
 [ScaffoldColumn(false)]
 public int RecipeID { get; set; }
 public string RecipeName { get; set; }

 public virtual ICollection<Ingredient> Ingredients { get; set; }
 public IEnumerable<int> SelectedIngredientIDs { get; set; }
 public ICollection<Ingredient> AllIngredients { get; set; }

 public Recipe()
 {
  Ingredients = new HashSet<Ingredient>();
 }
}

In the controller, I populate all ingredients from the database into the AllIngredients and then the IDs of the ingredients of the recipe into the SelectedIngredientIDs.

Recipe recipe = recipeDB.Recipes.Find(id);
recipe.AllIngredients = recipeDB.Ingredients.ToList();

recipe.SelectedIngredientIDs = Enumerable.Empty<int>();
foreach (Ingredient ing in recipe.Ingredients)
{
 recipe.SelectedIngredientIDs = recipe.SelectedIngredientIDs.Concat(new[] {ing.IngredientID});
}

Then I create a MultiSelectList as follows

@Html.ListBoxFor(model => model.SelectedIngredientIDs, new MultiSelectList(Model.AllIngredients, "IngredientID", "IngredientName"), new {Multiple = "multiple"})

Gotcha 2. Preselect the current ingredients in the list. The application works by now and the list is populated, but nothing is selected. Why is that? I checked the SelectedIngredientIDs and they are populated properly. The trick was to find out that MVC uses the ToString method as a way to determine if an item is selected or not, so I had to override it in the Ingredient class. Just added a piece of code below and it started working like magic.

public class Ingredient
{
 public int IngredientID { get; set; }
 
 ...
 
 public override string ToString()
 {
  return this.IngredientID.ToString();
 }
}

Gotcha 3. Finally, I had my list being populated and I also could change the selection to my content. However, no exceptions were thrown but also no updates were being saved to the database. The not-so-little trick was to find out how exactly to let the Entity Framework know what needs to be updated. Here is the slightly simplified HttpPost method (try/catch omitted etc) which worked, with comments.

[HttpPost]
public ActionResult Edit(Recipe recipe)
{
 if(ModelState.IsValid)
 {
  //get the id of the current recipe
  int id = recipe.RecipeID;
  //load recipe with ingredients from the database
  var recipeItem = recipeDB.Recipes.Include(r => r.Ingredients).Single(r => r.RecipeID == id);
  //apply the values that have changed
  recipeDB.Entry(recipeItem).CurrentValues.SetValues(recipe);
  //clear the ingredients to let the framework know they have to be processed
  recipeItem.Ingredients.Clear();
  //now reload the ingredients again, but from the list of selected ones as per model provided by the view
  foreach (int ingId in recipe.SelectedIngredientIDs)
  {
   recipeItem.Ingredients.Add(recipeDB.Ingredients.Find(ingId));
  }
  //finally, save changes as usual
  recipeDB.SaveChanges();
  return RedirectToAction("Index");
 }
 return View(recipe);
}

I'm anticipating the gotchas in the Create view already!

References I used

ASP.NET MVC MultiSelectList with selected values not selecting properly
MVC 3 Quirk: MultiSelect with selected items
Many-To-Many Relationship Basic Example (MVC3) by . Also posted on my website

Sunday, May 13, 2012

Learning MVC - Code First and Many-to-many Relationship

Learning MVC I came across a need to create a many-to-many relationship between entities using the code first approach. For an example, let's consider the Recipes database which holds recipes and ingredients. A recipe has multiple ingredients, such as meat, potatoes, pepper and so on. At the same time an ingredient may belong to multiple recipes, i.e. you can cook meat with potatoes but fish and chips will also require potatoes. That's a classic many-to-many relationship which has a classic mapping table solution, where one many-to-many relationship would convert to two one-to-many by adding a new table. The diagram would look like the following:

Typical database solution for many-to-many relationship

So my first guess was that I will have to create three classes while implementing a similar structure with MVC code first. Fortunately, so far it appears to be easier than that. Below is the small exercise that creates a most basic MVC project from scratch and illustrates the many-to-many relationship via code first.

Start Visual Studio 2010 and select File -> New Project, select ASP.NET MVC 3 Web Application. On the next screen I selected Empty application to make the example most simple.

Create an empty MVC 3 application

After the project was created, I added a HomeController to create a basic home page. Probably not necessary, but lets the application run without displaying an error. Right-click Controllers folder, select Add -> Controller and add an empty HomeController.

Add a HomeController

Inside the HomeController.cs, the Index() method, right-click and select Add View. Leave default values and click Add.

Create an Index page for the project

Run the project to verify that no errors are displayed. Now it is the time to add entities for the Recipe and Ingredient. Right-click Models folder, select Add -> Class and call the class Recipe.cs. Add another one called Ingredient.cs. The little trick with a many-to-many relationship is to create an ICollection within each of the two related entities. The collection will actually hold those "many" entities that are related to the particular instance. In our case - the Recipe holds a list of all Ingredients it uses.

Add new class in Models folder

public class Recipe
{
 public int RecipeID { get; set; }
 public string Name { get; set; }

 public virtual ICollection<Ingredient> Ingredients { get; set; }

 public Recipe()
 {
  Ingredients = new HashSet<Ingredient>();
 }
}

public class Ingredient
{
 public int IngredientID { get; set; }
 public string Name { get; set; }

 public virtual ICollection<Recipe> Recipes { get; set; }

 public Ingredient()
 {
  Recipes = new HashSet<Recipe>();
 }
}

And we'll need to have some sample data to verify that the application is working as expected. So add another class and call it SampleData.cs. Entity Framework allows to "seed" the newly created database. An implementation of DropCreateDatabaseIfModelChanges class will re-seed the database when model changes. This is handy for testing.

public class SampleData : DropCreateDatabaseIfModelChanges<RecipesEntities>
{
 protected override void Seed(RecipesEntities context)
 {
  var ingredient0 = new Ingredient{Name = "Meat"};
  var ingredient1 = new Ingredient{Name = "Fish"};
  var ingredient2 = new Ingredient{Name = "Potato"};

  var ingredients = new List<Ingredient>(){ingredient0, ingredient1, ingredient2};

  ingredients.ForEach(i => context.Ingredients.Add(i));

  var recipes = new List<Recipe>();
  
  recipes.Add(new Recipe{Name = "Grilled fish with potatoes", Ingredients = new List<Ingredient>() {ingredient1, ingredient2}});
  recipes.Add(new Recipe{Name = "Grilled steak with potatoes", Ingredients = new List<Ingredient>() {ingredient0, ingredient2}});

  recipes.ForEach(r => context.Recipes.Add(r));
 }
}

Next step was to create the database. Right-click Project, select Add -> Add ASP.NET Folder -> App_Data. The folder will be created. It will already have the correct security access settings.

Create an App_Data folder

Next, I added the following at the end of the web.config file just before the closing "configuration" tag. Now the Entity Framework will know how to connect to the database:

<connectionStrings>
 <add name="RecipesEntities"
 connectionString="Data Source=|DataDirectory|Recipes.sdf"
 providerName="System.Data.SqlServerCe.4.0"/>
</connectionStrings>

Next step is to create a context class, which will represent the Entity Framework database context. It is very simple indeed, and I'll create it by adding a new class called RecipeEntities.cs in the model folder. This class will be able to handle database operations due to the fact that it is extending DbContext. Here is the code:

public class RecipesEntities : DbContext
{
 public DbSet<Recipe> Recipes { get; set; }
 public DbSet<Ingredient> Ingredients { get; set; }
}

Now, with the model, database, context and some sample data in place, it is time to verify that data is actually displayed properly by the application. First, the recipes. I'm going to check that the list is displayed properly and that I can display all the details I'm interested in (for now, just the list of ingredients). For that, I'll need a RecipeController and two views, List and Details. First, the controller, that I will create with default methods for displaying, editing, creating and deleting data. I'm only interested in two methods, which I'll modify as follows

RecipesEntities recipeDB = new RecipesEntities();

// GET: /Recipe/
public ActionResult Index()
{
 var recipes = recipeDB.Recipes.ToList();
 return View(recipes);
}

// GET: /Recipe/Details/5
public ActionResult Details(int id)
{
 var recipe = recipeDB.Recipes.Find(id);
 return View(recipe);
}

Now I'll right-click within the Index method and select Add View, which I will configure as follows:

Create the List view

Now if I run the application as it is and navigate to Recipe (http://localhost/Recipe), I should see the list of recipes.

Display the list of ingredients

Next, I want to see the details. I'll add another view by right-clicking within the Details method of the controller and select Add View, which I will configure in the similar fashion:

Create the Details view

The default contents of the view will look similar to this:

@model RecipesSample.Models.Recipe
@{
    ViewBag.Title = "Details";
}

<h2>Details</h2>
<fieldset>
    <legend>Recipe</legend>
    <div class="display-label">Name</div>
    <div class="display-field">@Model.Name</div>
</fieldset>
<p>
    @Html.ActionLink("Edit", "Edit", new { id=Model.RecipeID }) |
    @Html.ActionLink("Back to List", "Index")
</p>

This, however, will only display the name of the recipe, but not the ingredients. To check that the ingredients are returned from the database properly, I have to modify the view to look similar to this:

@model Recipes.Models.Recipe
@{
    ViewBag.Title = "Details";
}

<h2>Details</h2>

<fieldset>
    <legend>Recipe</legend>
    <div class="display-label">Name</div>
    <div class="display-field">@Model.Name</div>
</fieldset>

<fieldset>
    <legend>Ingredients</legend>
    @foreach (Recipes.Models.Ingredient ingredient in Model.Ingredients)
    { 
        <div>@Html.DisplayFor(model => ingredient.Name)</div>
    }
</fieldset>

<p>
    @Html.ActionLink("Edit", "Edit", new { id=Model.RecipeID }) |
    @Html.ActionLink("Back to List", "Index")
</p>

Now if I run the application, navigate to Recipe and click Details on any of them (the link will point to http://localhost:49606/Recipe/Details/1 or similar), I will see the following page:

Display the related data

The recipe ingredients were successfully extracted from the model and displayed. As an exercise, it's easy to perform reverse action - check that if the ingredient is displayed, the recipes where it is used can also be shown. Hint: the view code may look similar to this:

@model Recipes.Models.Ingredient
@{
    ViewBag.Title = "Details";
}

<h2>Details</h2>

<fieldset>
    <legend>Ingredient</legend>
    <div class="display-label">Name</div>
    <div class="display-field">@Model.Name</div>
</fieldset>

<fieldset>
    <legend>Is used in the following recipes</legend>
    @foreach (Recipes.Models.Recipe recipe in Model.Recipes)
    { 
        <div>@Html.DisplayFor(model => recipe.Name)</div>
    }
</fieldset>
<p>
    @Html.ActionLink("Edit", "Edit", new { id=Model.IngredientID }) |
    @Html.ActionLink("Back to List", "Index")
</p>

And the output will be

Display the related data

This is a very crude example without following "best practices" (such as creating the DbContext inside the using statement) and without any formatting, but it shows that an application that employs the many-to-many relationships between the entities can be created with MVC with just several lines of code.

References I used:

Part 4: Models and Data Access
Creating a Many To Many Mapping Using Code First
The type or namespace name 'DbContext' could not be found
DropCreateDatabaseIfModelChanges Class by . Also posted on my website

Wednesday, May 2, 2012

Converting a Physical PC to VM with VMWare Converter

When the software I'm working on is installed on the PC which is later shipped to the client, the PC has an exact configuration and the operating system is set up in a certain way, which is precisely documented. To make sure every workstation has exactly the same configuration, the images of the hard disk partitions are created once and then copied over to every PC. There are cases, however, when the same configuration needs to be applied to the Virtual Machine - for example, to simplify some of the testing tasks. In this case the desired action is to use the preconfigured PC and to convert it to the Virtual Machine. Since we use the VMWare products, the tool that I use also comes from VMWare and it's called VMware vCenter Converter (aka vConverter Standalone).

To start with, I downloaded and installed VMware vCenter Converter (aka vConverter Standalone). In my case, Local installation was sufficient. In local mode I can only create and manage conversion tasks from the PC on which I install the converter. The client-server installation allows to create and manage conversion tasks remotely in case you need that.

Choose "Local Installation".

Two important configurations have to be set up on the source PC (the PC that is converted to VM) before the conversion starts. First, file sharing has to be disabled.

Turn off file sharing

Also, the Windows firewall has to be disabled.

Disable Windows Firewall

You should also check some other things, such as the Windows version being supported, network access, no other conversion jobs running on the source machine and no VMware Converter installations existing on the source PC. After all that is taken care of, I ran the client application. In the application, I selected Convert machine and filled in the details.

Source PC details

When the connection succeeded, I saw the following message. Just to make sure that I will not forget to remove the agent and to free myself from extra hassle, I chose the automatic version.

Select the uninstallation method for Converter Standalone

Next step was to provide the location where the virtual machine will be saved.

Destination System

Next step was to select what I wanted to copy. I wanted to copy the whole machine, with one small exclusion: there was a disk drive that only contained some backup data and was irrelevant. So it was possible to save some time and disk space this way. I unchecked the drive I was not interested in. If you are an advanced user, you can apply further configuration changes.

Conversion task options

Uncheck the hard disk

Next step was to review the details and press Finish. Everything went well and a job was be added to the list, displaying some details and estimated time to completion. Now all I needed was some patience.

Conversion task

When the conversion task was complete, I opened the newly created VM.

Open Virtual Machine

I got a warning message, which, I assumed, was related to the fact that VMWare tools were not yet installed. After I installed the tools later, I never saw the warning again.

Warning message

Finally the VMWare tools improve graphic performance on the guest PC. So the last step was to install them.

Install VMWare tools

In my case, a message appeared and I did exactly as it advised - on the VM, logged in and ran E:\setup.exe from the command prompt. The installation started and I followed the prompts until VMWare tools were installed.

VMWare message

by . Also posted on my website