Pages

Tuesday, December 25, 2012

Drag & Drop Tab Control

Recently I had to implement drag and drop tab control, so users would be able to reorder tab pages inside control by simple drag-n-drop. As any other developer I started to google and found this solution: CodeProject. It seemed quite nice to me, so I used it.
In our application we are using DevExpress controls and XtraTabControl as well, so my TabControl was inherited from DevExpress.XtraTab.XtraTabControl. It worked as expected, but little later we have found that on Windows 7 there is a bug. If someone would try to drag second tab into the first position there will be error:

Object reference not set to an instance of an object. System.NullReferenceException at DevExpress.XtraTab.Drawing.SkinTabPageObjectInfo.CalcColor(BaseTabPageViewInfo pInfo) at DevExpress.XtraTab.Drawing.SkinTabPageObjectInfo..ctor(SkinElement element, BaseTabPageViewInfo pInfo) at DevExpress.XtraTab.ViewInfo.SkinTabHeaderViewInfo.UpdatePageInfo(BaseTabPageViewInfo pInfo) at DevExpress.XtraTab.Drawing.SkinTabPainter.DrawHeaderPage(TabDrawArgs e, BaseTabRowViewInfo row, BaseTabPageViewInfo pInfo) at DevExpress.XtraTab.Drawing.BaseTabPainter.DrawPage(TabDrawArgs e, BaseTabRowViewInfo rowInfo, BaseTabPageViewInfo pInfo) at DevExpress.XtraTab.Drawing.BaseTabPainter.DrawHeaderRow(TabDrawArgs e, BaseTabRowViewInfo rowInfo) at DevExpress.XtraTab.Drawing.BaseTabPainter.DrawHeader(TabDrawArgs e) at DevExpress.XtraTab.Drawing.BaseTabPainter.DrawForeground(TabDrawArgs e) at DevExpress.XtraTab.Drawing.BaseTabPainter.Draw(TabDrawArgs e) at DevExpress.XtraTab.XtraTabControl.OnPaint(PaintEventArgs e) at System.Windows.Forms.Control.PaintWithErrorHandling(PaintEventArgs e, Int16 layer) at System.Windows.Forms.Control.WmPaint(Message& m) at System.Windows.Forms.Control.WndProc(Message& m) at DevExpress.Utils.Controls.ControlBase.WndProc(Message& m) at DevExpress.XtraTab.XtraTabControl.WndProc(Message& m) at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m) at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m) at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)

And the entire tab control becomes crossed out with the two diagonal red lines. Terrible. But much more interesting, that I was not able to reproduce this error on Windows XP!
However, I found the solution. This implementation has too complex way to reorder tabs, so here is my version of OnDragOver:

protected override void OnDragOver(System.Windows.Forms.DragEventArgs e)
{
    base.OnDragOver(e);

    Point pt = new Point(e.X, e.Y);
    //We need client coordinates.
    pt = PointToClient(pt);

    //Get the tab we are hovering over.
    XtraTabPage hover_tab = null;
    for (int i = 0; i < TabPages.Count; i++)
    {
        DevExpress.XtraTab.ViewInfo.XtraTabHitInfo info = this.CalcHitInfo(pt);
        if (info.HitTest == DevExpress.XtraTab.ViewInfo.XtraTabHitTest.PageHeader)
        {
            hover_tab = info.Page as XtraTabPage;
            break;
        }
    }

    //Make sure we are on a tab.
    if (hover_tab != null)
    {
        //Make sure there is a XtraTabPage being dragged.
        if (e.Data.GetDataPresent(typeof(XtraTabPage)))
        {
            e.Effect = DragDropEffects.Move;
            XtraTabPage drag_tab = (XtraTabPage)e.Data.GetData(typeof(XtraTabPage));

            if (drag_tab.Parent != this) //if we are trying to drag "aliens" tab - then stop!
            {
                e.Effect = DragDropEffects.None;
                return;
            }

            int item_drag_index = FindIndex(drag_tab);
            int drop_location_index = FindIndex(hover_tab);

            //Don't do anything if we are hovering over ourself.
            if (item_drag_index != drop_location_index)
            {
                //simply set child index!
                TabPages.SetChildIndex(hover_tab, item_drag_index);
                TabPages.SetChildIndex(drag_tab, drop_location_index);

                if (AfterDragNDrop != null) //there can be delegates for after Drag-N-Drop
                    AfterDragNDrop();

                //Make sure the drag tab is selected.
                SelectedTab = drag_tab;
            }
        }
    }
    else
    {
        e.Effect = DragDropEffects.None;
    }
}

Wednesday, December 19, 2012

Default parameters in delegates and reflection

Recently I have refactored a method. It was created in C# 2.0 and it was overriden many times to support default parameters, something like this:

public void Foo()
{
    Foo(0);
}

public void Foo(int arg1)
{
    Foo(arg1, "");
}

public void Foo(int arg1, string arg2)
{
    //finally, do something
}

But we have migrated to C# 4.0, so it is better to use actual default parameters, like this:

public void Foo(int arg1 = 0, string arg2 = "")
{
    //do something
}

However there are 2 questions. How it will work with delegates and how it will work with reflections. The answers are simple. You have to declare delegate with default parameter and for reflection you can use Type.Missing object. Here are 2 examples:

About delegates:
void Foo(int arg1 = 0, string arg2 = "")
{
    //do something
}

void Foo2(int arg1, string arg2)
{
    //do something
}

void FooError(int arg1)
{
    //do something
}

delegate void FooDelegate(int arg1 = 0, string arg2 = "");

void SomeWhere()
{
    //you can declare delegates with default parameters:
    FooDelegate method = Foo;
            
    //and invoke them:
    method();
    method(arg1: 10);
    method(arg2: "string value");

    //and you can even use functions without defaut parameters:
    FooDelegate method2 = Foo2;

    method2();
    method2(1, "string");

    //but you cannot use method with different number of arguments:
    FooDelegate method3 = FooError; //Here is error!
    method3();
}

And about reflection:
void SomeWhere()
{
    Type myType = typeof(MyClass); //for simplicity I will get Type like this
    var myObj = Activator.CreateInstance(myType); //create instance of the class
    var myMethodInfo = myType.GetMethod("Foo"); //and get method info from Type

    // now we can invoke that method with Type.Missing parameter for default value
    myMethodInfo.Invoke(myObj, new object[] { Type.Missing, Type.Missing });
    myMethodInfo.Invoke(myObj, new object[] { 10, Type.Missing });
    //however the order of the parameters must be correct!
    myMethodInfo.Invoke(myObj, new object[] { Type.Missing, "string" });

    myMethodInfo.Invoke(myObj, new object[] { "string", Type.Missing }); //this will throw an error!
}

Saturday, December 8, 2012

Composite pattern for testing applications

It is very common task to write some sort of testing application. It can be both an educational task and a commectial product. And the use of the composite pattern can dramatically simplify this task (of course if you are not making one-level tests). Composition pattern allows you to work with case studie as with any other kind of questions. You can read about this pattern here.
Here is my simple example of this pattern:

Classes for questions:
public abstract class TestTask
{
    public TestTask(string text)
    {
        answers = new List<TaskAnswer>();
        TaskText = text;
    }

    //abstract methods, common for every existing task
    public abstract void AddSubElement(TestTask element);
    public abstract void RemoveSubElement(TestTask element);
    public abstract bool CheckTask();
        
    //methods and properties for answers
    private List<TaskAnswer> answers;
    public string TaskText { get; set; }

    public void AddAnswer(params KeyValuePair<string, bool>[] newAnswers)
    {
        foreach (var answer in newAnswers)
        {
            answers.Add(new TaskAnswer(answer.Key, answer.Value));    
        }
    }
    public void RemoveAnswer(int number)
    {
        answers.RemoveAt(number);
    }
}

public class SingleChoiseTask: TestTask
{
    public SingleChoiseTask(string text) : base(text) {}

    //property for selected answer
    public TaskAnswer SelectedAnswer { get; set; }

    public override bool CheckTask()
    { //for single choise question just check if selected answer is correct
        return SelectedAnswer != null && SelectedAnswer.IsCorrect;
    }

    //single choise question cannot have any children
    public override void AddSubElement(TestTask element)
    {
        throw new NotImplementedException();
    }

    public override void RemoveSubElement(TestTask element)
    {
        throw new NotImplementedException();
    }
}

public class MultipleChoiseTask : TestTask
{
    public MultipleChoiseTask(string text): base(text)
    {
        SelectedAnswers = new List<TaskAnswer>();
    }

    //List for selected answers
    public List<TaskAnswer> SelectedAnswers;
    public override bool CheckTask()
    { //for multiple choise question we have to check that every selected answer is correct
        return SelectedAnswers.Count > 0 && SelectedAnswers.TrueForAll(answer => answer.IsCorrect);
    }

    // multiple choise question cannot have any children
    public override void AddSubElement(TestTask element)
    {
        throw new NotImplementedException();
    }

    public override void RemoveSubElement(TestTask element)
    {
        throw new NotImplementedException();
    }
}

public class  CaseStudieTask: TestTask
{
    public CaseStudieTask(string text): base(text)
    {
        SubTasks = new List<TestTask>();
    }

    //List of childrens
    private List<TestTask> SubTasks;

    public override void AddSubElement(TestTask element)
    {
        SubTasks.Add(element);
    }
    public override void RemoveSubElement(TestTask element)
    {
        SubTasks.Remove(element);
    }

    public override bool CheckTask()
    { // for case studie we have to check that every sub question is correct
        return SubTasks.TrueForAll(el => el.CheckTask());
    }
}
Class for answers:
public class TaskAnswer
{
    public string AnswerText { get; set; }
    public bool IsCorrect { get; set; }

    public TaskAnswer(string text, bool isCorrect)
    {
        AnswerText = text;
        IsCorrect = isCorrect;
    }
}
And example of usage:
public void InitializeTest()
{
    //Create container
    TestTask test = new CaseStudieTask("First test");
            
    //Initialise sample questions
    var firstQuestion = new SingleChoiseTask("First question");
    firstQuestion.AddAnswer(new KeyValuePair<string, bool>("correct answer", true),
                            new KeyValuePair<string, bool>("incorrect answer", false));

    var secondQuestion = new MultipleChoiseTask("Second question");
    secondQuestion.AddAnswer(new KeyValuePair<string, bool>("correct answer 1", true),
                                new KeyValuePair<string, bool>("correct answer 2", true));

    var thirdQuestion = new CaseStudieTask("Case studie");
    var firstSubQuestion = new SingleChoiseTask("First question in case studie");
    firstSubQuestion.AddAnswer(new KeyValuePair<string, bool>("correct answer", true),
                            new KeyValuePair<string, bool>("incorrect answer", false));
    var secondSubQuestion = new SingleChoiseTask("Second question in case studie");
    secondSubQuestion.AddAnswer(new KeyValuePair<string, bool>("correct answer", true),
                            new KeyValuePair<string, bool>("incorrect answer", false));
    thirdQuestion.AddSubElement(firstSubQuestion);
    thirdQuestion.AddSubElement(secondSubQuestion);

    // And populate test
    test.AddSubElement(firstQuestion);
    test.AddSubElement(secondQuestion);
    test.AddSubElement(thirdQuestion);
}
Of course this example is not full. There is no way to navigate throught test, no binding for the answers and no scores (user can pass the test only if he scores 100%).
And I would add singleton pattern for test (and this class would calculate current points, watch for current question etc.). If you are interested I can show more complex example.