QuickStart (II)

The source code for this quick start can be found in the form of a downloadable Visual Studio 2008 solution.

First of all, we have to define an activity in terms of questions and treatments. One of the simplest activity imaginable would be the following :
Activity.png
  • this activity is about getting a list of products that match a specified name.
  • the first state is a question that asks for the name.
  • the second (and last) state is a treatment that finds the corresponding states.
  • let's add to this a rule that specifies that the name we are looking for should not be empty (nor null).

For this activity, we'll reuse the Product class we have defined in the Domain Model quick start. The proper way to implement this would certainly be to add a FindByName method to this class and query the database, but we'll do it the Activity way this time, for demonstration purposes.

An activity manipulates data. We need a class to store :
  • the name of the product.
  • the list of products matching that name.
There is a base class in the Salamanca.DataActivities namespace that will help us do just that, by implementing the IActivityData interface :
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Salamanca.DataActivities;

namespace QuickStart
{
    public class Data:
        ActivityData
    {
        private struct Backup
        {
            public string Name;
            public Product[] Products;
        }

        protected override object CreateBackup()
        {
            Backup ret=new Backup();
            ret.Name=_Name;
            ret.Products=_Products.ToArray();
            return ret;
        }

        protected override void RestoreBackup(object backup)
        {
            Backup b=(Backup)backup;
            _Name=b.Name;
            _Products=new List<Product>(b.Products);
        }

        public string Name
        {
            get { return _Name; }
            set { _Name=value; }
        }

        public IList<Product> Products
        {
            get { return _Products; }
            set { _Products=new List<Product>(value); }
        }

        private string _Name;
        private List<Product> _Products=new List<Product>();
    }

}

You see there is a bit more than just having a Name and a Products property. When inheriting from ActivityData, you have to implement two abstract methods that create and restore backups on the data. We won't really need this system in this activity, but activity data have to implement the IBackupable interface and this is quite straightforward as you can see in the code above.

Our activity will ask a question. This question is going to be implemented in the UI layer (likely via a TextBox on Windows Forms), so we need a way to allow it to plug itself into our activity. We do that via a question factory, as follows :
using System;
using Salamanca.DataActivities;
using Salamanca.DataActivities.UI;

namespace QuickStart
{
    public abstract class QuestionFactory
    {
        public abstract Question AskProductName(IActivityController controller);
    }
}

Our question state will need this QuestionFactory (or, rather, an implementation of it). And I anticipate that our treatment state will need a DataMapperCollection as we will retrieve all the products from the database via the Product.FindAll method. These are the dependencies of our activities, and we will group them in a parameter class :
using System;
using Salamanca.DataAccess.Collections;
using Salamanca.DataActivities;

namespace QuickStart
{
    public class Parameters:
        IQuestionFactoryParameters<QuestionFactory>
    {
        public Parameters(QuestionFactory factory, DataMapperCollection dataMappers)
        {
            _QuestionFactory=factory;
            _DataMappers=dataMappers;
        }

        public DataMapperCollection DataMappers
        {
            get
            {
                return _DataMappers;
            }
        }

        public QuestionFactory QuestionFactory
        {
            get
            {
                return _QuestionFactory;
            }
        }

        private DataMapperCollection _DataMappers;
        private QuestionFactory _QuestionFactory;
    }
}

We are now ready to create our two states. The first one must implement IActivityInitialState, which is just a marker interface. As a question state, we can simply derive it from the QuestionActivityState base class. The second state just fills our Data with a list of products matching the name that was returned by the question :
using System;
using System.Collections.Generic;
using Salamanca.DataActivities;
using Salamanca.DataActivities.UI;
using Salamanca.DataRules;

namespace QuickStart
{
    public class FindProductListByName:
        QuestionActivityState<Data>,
        IActivityInitialState
    {
        public FindProductListByName(Data data, QuestionFactory factory, DataMapperCollection dataMappers):
            base(data, new Parameters(factory, dataMappers))
        {
        }

        protected override IList<IRule> CreateRules()
        {
            IList<IRule> ret=base.CreateRules();
            ret.Add(
                new PredicateRule<FindProductListByName>(
                    "The name must not be empty.",
                    new Predicate<FindProductListByName>(
                        delegate(FindProductListByName s) {
                            return !string.IsNullOrEmpty(s.Data.Name);
                        }
                    )
                )
            );

            return ret;
        }

        protected override void OnInitialized(ActivityStateEventArgs e)
        {
            Question=((Parameters)Parameters).QuestionFactory.AskProductName(e.Controller);
            base.OnInitialized(e);
        }

        protected override IActivityState NextState
        {
            get { return new FindProductListByNameFindProducts(Data, (Parameters)Parameters); }
        }
    }

    internal class FindProductListByNameFindProducts:
        ActivityState<Data>
    {
        public FindProductListByNameFindProducts(Data data, Parameters parameters) :
            base(data, parameters)
        {
        }

        protected override ActivityStateResult Handle(IActivityController controller)
        {
            List<Product> allProducts=new List<Product>(Product.FindAll(((Parameters)Parameters).DataMappers));
            Data.Products=allProducts.FindAll(
                new Predicate<Product>(
                    delegate(Product p) {
                        return p.Name.Contains(Data.Name);
                    }
                )
            );

            return ActivityStateResult.Next;
        }

        protected override IActivityState NextState
        {
            get { return new EndActivityState<Data>(Data); }
        }
    }
}

Note in the question state how we enforced our business rule (the name must not be empty).

Our activity is ready. Or is it ? What we have here is still an abstraction, and we need an implementation for our QuestionFactory. This is precisely the magic of it all : we just need a concrete QuestionFactory.

Say, for instance, that we want to test our activity : we then need a question factory that automatically provides the Data with test product names and checks the results. Here is an implementation of such a question factory :
internal sealed class TestQuestionFactory:
    QuestionFactory
{
    public TestQuestionFactory(string name)
    {
        _Name=name;
    }

    public override Question AskProductName(IActivityController controller)
    {
        Question ret=new AnsweredQuestion();
        ret.Answering+=new EventHandler<AnswerEventArgs>(
            delegate(object sender, AnswerEventArgs e) {
                ((Data)e.Data).Name=_Name;
            }
        );

        return ret;
    }

    private string _Name;
}

Based on this factory, a test would look like this (there are two products containing Queso in their name in the Northwind database) :
[TestMethod]
public void CanFindProductList()
{
    Data data=new Data();
    ActivityController controller=new ActivityController(
        new FindProductListByName(
            data,
            new TestQuestionFactory("Queso"),
            DataMappers
        )
    );
    controller.Execute();

    Assert.IsTrue(controller.HasCompleted);
    Assert.AreEqual(2, data.Products.Count);
}

You can see here how an ActivityController is used to execute an activity. As all answers are automatically provided, our activity is executed from the beginning to the end in one phase. In a Windows Forms application, the execution would stop at the question state to let the user fill a text box in. When a Find button would be clicked, the controller would be told to resume the activity execution.

You can download a complete working example, as a Visual Studio 2008 solution. The same activity is implemented as a unit test, and as a Windows Forms application (the underlying database is Northwind on SQL Server, for which a data file is included in the downloadable sample).

Last edited Oct 22, 2009 at 8:17 AM by cartoixa, version 3

Comments

No comments yet.