HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

Mixin and template-heavy code for a Silverlight clone

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
heavytemplatemixincodesilverlightforandclone

Problem

I've made a mock version of Silverlight in D2. It is meant to output HTML, but since I don't know HTML yet, its current output is in. HTML is not the reason I'm here, though. The goal of my project was to create a framework that uses XAML and MVVM to create a site bound to a viewmodel. It doesn't do too much and isn't dynamic as of yet, but right now I'm just at the first milestone.

Before I go any further with the project I'd like some feedback on what I have so far. Specifically, my use of template mixins to achieve polymorphism and override base class methods, even if they contain the exact same code (this problem is explained in code-comments). But at the same time this is my first forray into D and I'm sure there are other things I could use help with, so I'm open to any suggestions.

The code is on GitHub for anyone interested. I know it's not very useful but I'm having fun with it.

In my opinion, the code explains itself with comments, but I can add more if it's unclear.

```
module darklight;
import std.stdio: writeln;
enum string __module = "darklight";

//A sample view model class
//Can compound classes other than OddVM as well, this is just a demo
class OddVM {
string text;
OddVM inner;
OddVM list[];

string amethod(string name) {
return "my name is: "~name;
}

this(string te) {
this.text = te;
}
}

//showing off the different controls, as well as binding features and some of the xml capacity
enum string _xml = "










";
//viewModel when accessed provides the passed in vm
//binding is the new one if set, or the same as viewModel if not

int main(string[] args) {
//create a sample viewModel
OddVM vm = new OddVM ("bound");
vm.inner = new OddVM ("bound inner");
vm.inner.inner = new OddVM ("bound inner inner");

OddVM vm1 = new OddVM("first");
OddVM vm2 = new OddVM("second");
OddVM vm3 = new OddVM("third");
vm.inner.list = [vm1, vm2, vm

Solution

Here's what I've settled with in case anyone is interested:

Using the factory pattern I was able to build an injectable factory. Then using dependency injection I forward the factory through all the control extraction steps. This has the advantage of allowing a control class to instantiate a sub-control inside itself without worrying about importing the proper modules (and avoiding circular references).

The extract method is now able to access all the member variables because it is passed the type (as ForwardRefT), instead of using the class's own, bypassing the lack of polymorphism for templated methods. Thankfully, this also removes the need to add template mixins everywhere, which was ugly.

Now that you've read it and are fully prepared for the wackyness to come, here is the code:

//This factory is mixed in once, at the highest level of your web app.
//This way it can reference every module with controls in it and inject itself into them, giving them that ability as well.
mixin template FactoryGen() {
    import darklight.parse : parse_tag;
    struct ControlFactory
    {
        static auto Extract(string _xml, T)(T viewModel)
        {
            //parse_tag takes " ...  and returns "StackPanel"
            enum Ty = parse_tag!(_xml);
            mixin("return "~Ty~".extract!(ControlFactory, _xml, "~Ty~", T)(viewModel);");
        }
    }
}

abstract class Control
{
    override abstract string toString();

    static auto extract(alias F, string _xml, ForwardRefT, T)(T viewModel)
    {
        ForwardRefT self = new ForwardRefT();
        //parse_attr takes " ... " and returns {0}
        mixin(assignstring(parse_attr!(_xml))); 
        return self;
    }
}

abstract class ContainerControl(int I=0) : Control
{
    static if (I == 0)
    {
        Control[] controls;
    }
    else
    {
        Control[I] controls;
    }

    override string toString()
    {
        string content;
        static if (I == 0)
        {
            content = reduce!("a~b.toString()")("",this.controls);
        }
        else
        {
            foreach(control; this.controls)
            {
                content ~= control.toString();
            }
        }

        return format(`
%s
`, content);
    }
}

abstract class StackControl(int I=0) : ContainerControl!(I)
{
    static auto extract(alias F, string _xml, ForwardRefT, T)(T viewModel)
    {
        ForwardRefT self = new ForwardRefT();
        mixin(assignstring(parse_attr!(_xml)));
        //parse_inner takes " {0} " and returns {0}
        //subparse takes "" and returns ["",""]
        enum contents = subparse(parse_inner!(_xml));
        static assert(I == 0 || contents.length == I);
        Chain!(F, contents)(self, vm);
        return self;
    }

    static void Chain(alias F, alias inners, ForwardRefT, T)(ForwardRefT self, T viewModel)
    {
        static if (inners.length > 0)
        {
            Control next = F.Extract!(inners[0])(viewModel);
            static if (I == 0)
            {
                self.controls ~= next;
            }
            else
            {
                self.controls[I-inners.length] = next;
            }

            Chain!(F, inners[1..$])(self, viewModel);
        }
    }
}

class StackPanel : StackControl!() { }

class ListPanel : ContainerControl!(0)
{   
    static auto extract(alias F, string _xml, ForwardRefT, T)(T viewModel)
    {
        ForwardRefT self = new ForwardRefT();
        //assignstringPlus is likeassignstring but makes sure there is a variable named "list", defaulting it to null if it isn't in the xml
        mixin(assignstringPlus(parse_attr!(_xml),"list","null"));
        enum contents = subparse(parse_inner!(_xml));
        static assert(contents.length == 1);
        foreach(item; list)
        {
            self.controls ~= F.Extract!(contents[0])(item);
        }

        return self;
    }
}


I'm very pleased with the results as it makes user code much more manageable and cleaner without the mixins. The extract method may look nasty, but the only really scary part is the method signature. Users shouldn't ever have to write another extract method, but if they do it should be much easier than it was in the past.

Code Snippets

//This factory is mixed in once, at the highest level of your web app.
//This way it can reference every module with controls in it and inject itself into them, giving them that ability as well.
mixin template FactoryGen() {
    import darklight.parse : parse_tag;
    struct ControlFactory
    {
        static auto Extract(string _xml, T)(T viewModel)
        {
            //parse_tag takes "<StackPanel ...> ... </StackPanel> and returns "StackPanel"
            enum Ty = parse_tag!(_xml);
            mixin("return "~Ty~".extract!(ControlFactory, _xml, "~Ty~", T)(viewModel);");
        }
    }
}

abstract class Control
{
    override abstract string toString();

    static auto extract(alias F, string _xml, ForwardRefT, T)(T viewModel)
    {
        ForwardRefT self = new ForwardRefT();
        //parse_attr takes "<StackPanel {0}> ... </StackPanel>" and returns {0}
        mixin(assignstring(parse_attr!(_xml))); 
        return self;
    }
}

abstract class ContainerControl(int I=0) : Control
{
    static if (I == 0)
    {
        Control[] controls;
    }
    else
    {
        Control[I] controls;
    }

    override string toString()
    {
        string content;
        static if (I == 0)
        {
            content = reduce!("a~b.toString()")("",this.controls);
        }
        else
        {
            foreach(control; this.controls)
            {
                content ~= control.toString();
            }
        }

        return format(`
<div>%s
</div>`, content);
    }
}

abstract class StackControl(int I=0) : ContainerControl!(I)
{
    static auto extract(alias F, string _xml, ForwardRefT, T)(T viewModel)
    {
        ForwardRefT self = new ForwardRefT();
        mixin(assignstring(parse_attr!(_xml)));
        //parse_inner takes "<StackPanel ...> {0} </StackPanel>" and returns {0}
        //subparse takes "<1></1><2></2>" and returns ["<1></1>","<2></2>"]
        enum contents = subparse(parse_inner!(_xml));
        static assert(I == 0 || contents.length == I);
        Chain!(F, contents)(self, vm);
        return self;
    }

    static void Chain(alias F, alias inners, ForwardRefT, T)(ForwardRefT self, T viewModel)
    {
        static if (inners.length > 0)
        {
            Control next = F.Extract!(inners[0])(viewModel);
            static if (I == 0)
            {
                self.controls ~= next;
            }
            else
            {
                self.controls[I-inners.length] = next;
            }

            Chain!(F, inners[1..$])(self, viewModel);
        }
    }
}

class StackPanel : StackControl!() { }

class ListPanel : ContainerControl!(0)
{   
    static auto extract(alias F, string _xml, ForwardRefT, T)(T viewModel)
    {
        ForwardRefT self = new ForwardRefT();
        //assignstringPlus is likeassignstring but makes sure there is a variable named "list", defaulting it to null if it isn't in the xml
        mixin(assignstringPlus(parse_attr!(_xml),"list","null"));
        enum contents

Context

StackExchange Code Review Q#7438, answer score: 3

Revisions (0)

No revisions yet.