Thursday, June 14, 2012

Learning MVC: Unit Testing Validation in MVC

This is a rather short post. A little "trick" is required to test validation. If the Create function is called directly from the unit test, and the entity is validated using DataAnnotation validation, the model binder will not be invoked and the validation will not take place. The test result, obviously, will not be the expected one. To deal with that, it is necessary to mimic the behavior of the model binder. The following test creates a ValidationContext and uses the DataAnnotations.Validator class to validate the model.

[TestMethod()]
public void ValidateNameIsTooShort()
{
 Ingredient ingredient = new Ingredient() { IngredientName = "a" };

 var validationContext = new ValidationContext(ingredient, null, null);
 var validationResults = new List<ValidationResult>();
 Validator.TryValidateObject(ingredient, validationContext, validationResults);

 string error = GetValidationError(validationResults);

 Assert.AreEqual(error, Constants.Constants.IngredientNameTooShort);
}

If any errors are caught by the validation, they will be added to the ModelState of the controller. To get it back and compare with my expected error message, I just need to retrieve the first error message from the list of ValidationResult.

public string GetValidationError(List<ValidationResult> results)
{
 foreach (var result in results)
 {
  if(!String.IsNullOrEmpty(result.ErrorMessage))
  {
   return result.ErrorMessage;
  }
 }
 return string.Empty;
}

References:

Testing DataAnnotation-based validation in ASP.NET MVC
Unit tests on MVC validation
by . Also posted on my website

Tuesday, June 12, 2012

Learning MVC: Unit Testing CRUD actions in MVC

Continuing the example with a recipe database, and having the most basic structure working (recipes belong to categories and consist of ingredients), it is time to remember about TDD (test driven development). To be honest, it is already too late, because in proper TDD tests are supposed to be writter before the code is. I'll do my best next time. This time, however, there are some simple tests I can think of which relate to the Ingredient entity

  • Test that an Ingredient can be inserted into the database
  • Test that an Ingredient can be edited
  • Test that an Ingredient can be deleted from the database
  • Test that an Ingredient can not be deleted if it is used by any Recipe
  • Test that an Ingredient with a name that is too short can not be created
  • Test that an Ingredient with a name that is too long can not be created
  • Test that an existing Ingredient can not be edited so that its name becomes too short
  • Test that an existing Ingredient can not be edited so that its name becomes too long

In fact, the last four tests are not related to database manipulations because the errors will be caught before the attempt to save data is made. It would be more appropriate to place them in a separate class, which will test the model and I'll return to them next time. The first four tests appear to test database operations.

Firstly, I would like to have each test start from a known state - for example, an almost empty database which has some minimal amount of seed data. To achieve that, I use the initializer class that inherits from DropCreateDatabaseAlways. For reference, here is the full listing - the only thing I care about is to override the Seed function. I will use the data to test that I can not delete the ingredient which is in use.

public class TestDatabaseInitializer : DropCreateDatabaseAlways<RecipesEntities>
{
 protected override void Seed(RecipesEntities context)
 {
  var category0 = new Category { CategoryName = "Mains", Description = "Main Dishes" };
  var category1 = new Category { CategoryName = "Desserts", Description = "Dessert Dishes" };
  var categories = new List<Category>() { category0, category1 };
  categories.ForEach(c => context.Categories.Add(c));

  var ingredient0 = new Ingredient { IngredientName = "Meat" };
  var ingredient1 = new Ingredient { IngredientName = "Fish" };
  var ingredient2 = new Ingredient { IngredientName = "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 { RecipeName = "Grilled fish with potatoes", Category = category0, Ingredients = new List<Ingredient>() { ingredient1, ingredient2 } });
  recipes.Add(new Recipe { RecipeName = "Grilled steak with potatoes", Category = category0, Ingredients = new List<Ingredient>() { ingredient0, ingredient2 } });
  recipes.ForEach(r => context.Recipes.Add(r));
 }
}

I added a small function to my test class to create a database.

[TestInitialize()]
public void SetupDatabase()
{
 Database.DefaultConnectionFactory = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0");
 Database.SetInitializer<RecipesEntities>(new TestDatabaseInitializer());
}

Now I think I'm ready to create a simple test.

[TestMethod()]
public void CreateTest()
{
 SetupDatabase();
 IngredientController target = new IngredientController();
 Ingredient ingredient = new Ingredient() { IngredientName = "test" };

 ActionResult actual = target.Create(ingredient);
 Assert.IsTrue(ingredient.IngredientID != 0);

 RecipesEntities db = new RecipesEntities();
 var newIngredient = db.Ingredients.Find(ingredient.IngredientID);
 Assert.AreEqual(ingredient.IngredientName, newIngredient.IngredientName);
}

I create the Ingredient, record it's IngredientID and make sure that I can retrieve it back from the database by ID after it's added. Deletion and editing tests are equally simple. Now the "negative" test: I'm not testing what I can do now, but rather what I can not do. I should not be able to delete the ingredient if it is used by any recipes - that would destroy referential integrity. Here is a simple test that passes:

[TestMethod()]
public void CanNotDeleteUsedIngredient2()
{
 SetupDatabase();
 IngredientController target = new IngredientController();
 RecipesEntities db = new RecipesEntities();
 var ingredient = db.Ingredients.Where(i => i.IngredientName == "Meat").FirstOrDefault();
 int id = ingredient.IngredientID;

 Assert.IsNotNull(ingredient);
 ActionResult actual = target.DeleteConfirmed(id);

 db = new RecipesEntities();
 var deletedIngredient = db.Ingredients.Find(id);
 Assert.IsNotNull(deletedIngredient);
}

Okay, I tried to delete the ingredient and then I verified and it is still in the database. That's expected, but that tells me nothing about the reason why the ingredient was not deleted. Maybe the whole database is offline or there is an uncommitted transaction. To improve the test, I would like to know something about the reason. I need to check for the errors in the ModelState. Fortunately, I can access the ModelState from the ActionResult. Here is what I could do to return the first error that is found in the ModelState:

public string GetFirstErrorMessage(ActionResult result)
{
 ViewResult vr = (ViewResult)result;

 foreach (ModelState error in vr.ViewData.ModelState.Values)
 {
  foreach (var innerError in error.Errors)
  {
   if (!string.IsNullOrEmpty(innerError.ErrorMessage))
   {
    return innerError.ErrorMessage;
   }
  }
 }
 return string.Empty;
}

Now I can modify the test to check the ErrorMessage. The check is rather lame at the moment - the error message is created dynamically to tell the user what recipes exactly use the ingredient. So I do not want to check the full error message and I'm satisfied with the fact that the first 10 characters are what I expect. Here is the slightly modified test:

[TestMethod()]
public void CanNotDeleteUsedIngredient()
{
 SetupDatabase();
 IngredientController target = new IngredientController();
 RecipesEntities db = new RecipesEntities();
 var ingredient = db.Ingredients.Where(i => i.IngredientName == "Meat").FirstOrDefault();
 int id = ingredient.IngredientID;

 Assert.IsNotNull(ingredient);

 ActionResult actual = target.DeleteConfirmed(id);
 Assert.AreEqual(GetFirstErrorMessage(actual).Substring(0, 10), "Cannot del");

 db = new RecipesEntities();
 var deletedIngredient = db.Ingredients.Find(id);
 Assert.IsNotNull(deletedIngredient);
}

References:

Code First Entity Framework Unit Test Examples
Exercise 2: Testing CRUD actions
How to get all Errors from asp.net mvc modelState?
How to get the Model from an ActionResult? by . Also posted on my website

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

Wednesday, April 4, 2012

QML ScrollBar

I had to implement a ScrollBar component but the examples I could find did not do what I needed. I needed the ScrollBar with arrows on the sides, which would move the contents of the field a certain number of points to the required direction. After that, I added the sliding feature, that moved the contents of the field when the slider is dragged.

The ScrollBar consists of four rectangles. Two of them are arrows at the ends. One is the whole length of the scroll bar, and the last one is the slider that can be dragged along the body of the scroll bar. I used the Flickable component because I need to know the size of the field contents. Otherwise, I disabled the functionality of the flickable. When any of the arrows is clicked, first action is to identify if the slider can move in the required direction. This is calculated based on the height/width of the field (returned by flickable.height or width), height/width of the contents of the field (returned by flickable.contentHeight or contentWidth) and the current position of the contents within the flickable control (returned by flickable.contentX or contentY). If there is space to move, the flickable.contentX or contentY is changed, which moves the contents inside the field, and the slider position is adjusted.

The slide operates on the similar principle. When it is released, its position is read and then basing on the position and the length of the slider body the percentage of length the slider has traveled is calculated. From that percentage, the flickable.contentX or contentY is calculated so that the field contents move to reflect that percentage. It is not yet completely presice, but it does what is expected to do.

To use the ScrollBar, it needs to know several things: the id of the Flickable component associated with the field, the desired width of the scroll bar, the amount of pixels the contents need to move each click and the orientation - vertical or horisontal. In the example below, the Flickable component called view is associated with both scroll bars, the width of the scroll bar is 10 and the image moves 20 pixels each arrow click.

import QtQuick 1.1

Rectangle {
width: 360
height: 360

Flickable{

id:view
anchors.fill: parent
contentWidth: picture.width
contentHeight: picture.height
interactive: false

Image {
id: picture
source: "images/Desert.jpg"
asynchronous: true
}
}

ScrollBar{
id: verticalScroll
flickable: view
step: 20
size: 10
orientation: Qt.Vertical
}

ScrollBar{
id: horisontalScroll
flickable: view
step: 20
size: 10
orientation: Qt.Horizontal
}
}

QML ScrollBar

The listing of the ScrollBar component is as follows:

import QtQuick 1.1

Rectangle {

id: scrollBar
property variant orientation
property variant flickable
property int step
property int size
height: orientation === Qt.Vertical ? flickable.height : size
width: orientation === Qt.Vertical ? size : flickable.width

function canMove(first)
{
if(first)
return orientation === Qt.Vertical ? flickable.contentY > 0 : flickable.contentX > 0;
else
return orientation === Qt.Vertical ? flickable.height + flickable.contentY < flickable.contentHeight :
flickable.width + flickable.contentX < flickable.contentWidth
}

function arrowClicked(first)
{
if(canMove(first))
{
if(first)
{
if(orientation === Qt.Vertical)
flickable.contentY -= step;
else
flickable.contentX -= step;
}
else
{
if(orientation === Qt.Vertical)
flickable.contentY += step;
else
flickable.contentX += step;
}
positionSlider();
}
}

function moveContents()
{
if(orientation === Qt.Vertical)
flickable.contentY = (slider.y - size)*(flickable.contentHeight - flickable.height)/(body.height - slider.height);
else
flickable.contentX = (flickable.contentWidth - flickable.width)*slider.x/flickable.width;
}

function positionSlider()
{
var percentage = orientation === Qt.Vertical ? (flickable.contentY)/(flickable.contentHeight - flickable.height)
: (flickable.contentX + size)/(flickable.contentWidth - flickable.width);
if(orientation === Qt.Vertical)
slider.y = percentage*(body.height - slider.height) + size;
else
slider.x = percentage*(body.width - slider.width) + size;
}

onHeightChanged: {
if(canMove(true) || canMove(false))
positionSlider();
}

onWidthChanged: {
if(canMove(true) || canMove(false))
positionSlider();
}

Component.onCompleted: {
sliderArea.drag.axis = orientation === Qt.Vertical ? Drag.YAxis : Drag.XAxis;

if(orientation === Qt.Vertical)
{
scrollBar.anchors.right = flickable.right
firstArrow.anchors.top = scrollBar.top;
body.anchors.top = firstArrow.bottom;
secondArrow.anchors.bottom = scrollBar.bottom;
slider.y = size;
}
else
{
scrollBar.anchors.bottom = flickable.bottom;
firstArrow.anchors.left = scrollBar.left;
body.anchors.left = firstArrow.right;
secondArrow.anchors.right = scrollBar.right;
slider.x = size;
}
}

Rectangle{
id: firstArrow
width: size
height: size

Image{
id: imgFirstArrow
anchors.fill: parent
source: orientation === Qt.Vertical ? "images/vertUpArrow.png" : "images/horisLeftArrow.png"
}

MouseArea{
id: firstArrowArea
anchors.fill: parent
onClicked: {
arrowClicked(true);
}
}
}

Rectangle{
id: body
width: orientation === Qt.Vertical ? size : scrollBar.width - 2*size
height: orientation === Qt.Vertical ? scrollBar.height - 2*size : size
color: "#575B5E"
}

Rectangle{
id: slider
width: orientation === Qt.Vertical ? size : 3*size;
height: orientation === Qt.Vertical ? 3*size : size;

Image{
id: imgSlider
anchors.fill: parent
source: orientation === Qt.Vertical ? "images/SliderVert-Enabled.png" : "images/SliderHoris-Enabled.png"
}

MouseArea{
id:sliderArea
anchors.fill: parent
drag.target: slider
drag.minimumX: 10
drag.minimumY: 10
drag.maximumX: orientation === Qt.Vertical ? 0 : body.width - slider.width + size
drag.maximumY: orientation === Qt.Vertical ? body.height - slider.height + size : 0

onReleased: {
moveContents();
}
}
}

Rectangle{
id: secondArrow
width: size
height: size

Image{
id: imgSecondArrow
anchors.fill: parent
source: orientation === Qt.Vertical ? "images/vertDownArrow.png" : "images/horisRightArrow.png"
}

MouseArea{
id: secondArrowArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
arrowClicked(false);
}
}
}
}

References:

ScrollBar.qml Example File
Scrollable and Scroll indicators with QML by . Also posted on my website

Sunday, April 1, 2012

Return Multiple Values to QML from C++

In some cases it is required to return several values from a C++ function to the QML code. In my case, I call the function that checks the syntax of the Qt script that is passed as a string. The QScriptSyntaxCheckResult class does that check for me. If an error is found inside the script, I want to get back the error message and the line and column where the error was detected. In this case a QVariantMap class is the elegant and effective solution. In my C++ code, I pack the values I want to return into an instance of a QVariantMap class:

QVariantMap QMLFile::checkScriptSyntax(QString input) const
{
QScriptSyntaxCheckResult result = QScriptEngine::checkSyntax(input);

QVariantMap value;
value.insert("errorMessage", result.errorMessage());
value.insert("errorLineNumber", result.errorLineNumber());
value.insert("errorColumnNumber", result.errorColumnNumber());
return value;
}

In my QML code, I can now access the values in the following manner, after opening a file that contains a Qt script and passing its contents to the C++ function:

onClicked: {
var fileContent = QMLFile.getFileContents();
var result = QMLFile.checkScriptSyntax(fileContent);

if(result.errorMessage !== "")
{
console.log("Error on line " + result.errorLineNumber + ", column " + result.errorColumnNumber + " : " + result.errorMessage);
}
else
{
console.log("Script syntax is correct!");
}
}

The result is further used in the ToolTipRectangle element, which is a Text element that appears when the mouse is hovered over the line which contains an error.

import QtQuick 1.1

Rectangle {

property string toolTip : ""
property bool showToolTip: false

Rectangle{
id: toolTipRectangle
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.bottom
width: parent.toolTip.length * toolTipText.font.pixelSize / 2
height: toolTipText.lineCount * (toolTipText.font.pixelSize + 5)
z:100

visible: parent.toolTip !== "" && parent.showToolTip
color: "#ffffaa"
border.color: "#0a0a0a"

Text{
id:toolTipText
width: parent.parent.toolTip.length * toolTipText.font.pixelSize / 2
height: toolTipText.lineCount * (toolTipText.font.pixelSize + 5)
text: parent.parent.toolTip
color:"black"
wrapMode: Text.WordWrap
}
}

MouseArea {
id: toolTipArea
anchors.fill: parent
onEntered: {
parent.showToolTip = true
}
onExited: {
parent.showToolTip = false
}
hoverEnabled: true
}
}

The end result looks like this:

Syntax check result

References:

Q_INVOKABLE member function: Valid argument types
Implementing tool tips in QML by . Also posted on my website