Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

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


I've made a mock version of silverlight in D2. It is meant to output html but since I don't know html yet, it's current output is not valid. 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 view model. 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.
IMO 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 = "
<StackPanel name=viewModel.text
        binding=viewModel.inner>
    <TextBox name=\"itext1\"
        text=binding.text>
    </TextBox>
    <TextBox name=\"itext2\"
        binding=viewModel.inner
        text=binding.text
        />
    <TextBox name=\"itext3\"
        binding=viewModel.inner
        text=binding.amethod(\"robot\") />
    <TextBox name=\"itext4\"
        binding=viewModel.inner.inner
        text=\"too deep, would error if I tried binding\"/>
    <ListPanel name=\"somename\"
        binding=viewModel.list>
        <TextBox name=\"itextinside\"
            text=binding.text/>
    </ListPanel>
    <DoubleText name=\"double\"
        binding=viewModel.inner/>
</StackPanel>";
//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, vm3];

    //The vm could be any type of object
    //It just needs the proper variables as defined in the xml
    auto res = Control.BuildControl!(_xml)(vm);
    writeln(res.content());

    //change some values in the viewmodel
    vm.text = "new text";
    vm.inner.text = "new inner text";
    //can add things to a list and the templates won't be disturbed
    vm.inner.list ~= vm1;

    //need to rebuild controls to bind new values
    res = Control.BuildControl!(_xml)(vm);
    writeln(res.content());

    return 0;
}

abstract class Control {
    string name;

    mixin template codegen(string _xml) {
        //provides access to some parsed sections of the xml
        static const string extr[] = parsexml!(_xml);
        static const string _tag = extr == [] ? "" : extr[0];
        static const string _attr = extr == [] ? "" : extr[1];
        static const string _inner = extr == [] ? "" : extr[2];
        static const string _rem = extr == [] ? "" : extr[3];
        static const string _elem = extr == [] ? "" : extr[4];
    }

    mixin template setvarsgen() {
        private auto setvars(string _xml, T)(T viewModel) {
            //for access to attr
            mixin codegen!(_xml);
            //add the string of assignments for binding to the viewModel
            mixin(assignstring(_attr));
            return binding;
        }
    }

    //This is in a mixin template because when I want it to be called by a subclass
    //the subclass needs to have it's own implementation of it or the method bindings will crash.
    //ex: if this was called from TextBox without beind re-mixed-in, it would fail on assigning this.text
    //This might be a failure of polymorphism with templated methods, or just me
    mixin template extractgen(string _newxml = null) {
        static if (_newxml == null) {
            //a base extraction method, sets variables to the bindings outlined in xml
            private auto extract(string _xml, T)(T viewModel) {
                this.setvars!(_xml)(viewModel);
                return this;
            }
        } else {
            // This one is for controls with custom xaml, usually you shouldn't need to override this
            // But you'll probably have to do Control.extractgen!(_newxaml) to bypass any overrides
            private auto extract(string _xml, T)(T viewModel) {
                // The var setting from this gets overwritten, but we need the binding
                auto binding = setvars!(_xml)(viewModel);
                // Distinction here is that _newxml replaces xml
                auto ret = super.extract!(_newxml)(binding);
                // This lets xml bindings (ex: name) from the outside override anything on the inside
                setvars!(_xml)(viewModel);
                return ret;
            }
        }
    }

    mixin setvarsgen!();
    mixin extractgen!();

    //Outputs the html/other things representing the control
    abstract pure string content();

    static auto BuildControl(string _xml, T)(T viewModel) {
        mixin codegen!(_xml);
        mixin(_tag~" control = cast("~_tag~") Object.factory(\""~__module~"."~_tag~"\");");

        return control.extract!(_xml)(viewModel);
    }
}

class TextBox : Control {
    string text;

    //the new class variable text means these two have to be re-added
    mixin setvarsgen!();
    mixin extractgen!();

    pure string content() {
        return "<p "~this.name~">" ~ this.text ~ "<p>\n";
    }
}

class StackPanel : Control {
    Control controls[];

    mixin template extractgen() {
        //overrides the extract method, adds inner controls
        private auto extract(string _xml, T)(T viewModel) {
            mixin codegen!(_xml);
            auto binding = setvars!(_xml)(viewModel);
            this.controls = this.chain!(_inner)(binding);
            return this;
        }
    }

    mixin setvarsgen!();
    mixin extractgen!();

    pure string content() {
        string ret;
        foreach(contr; controls) {
            ret ~= contr.content();
        }
        return "<div "~this.name~">\n"~ret~"<div>\n";
    }

    private Control[] chain(string _xml, T)(T viewModel) {
        mixin codegen!(_xml);
        //this 5 is completely arbitrary, limits you w.r.t. the xml
        static if (_rem.length > 5) {
            return [cast(Control)BuildControl!(_elem)(viewModel)] ~ chain!(_rem)(viewModel);
        } else {
            return [cast(Control)BuildControl!(_xml)(viewModel)];
        }
    }
}

//example specialization of existing control
class ListPanel : StackPanel {
    //this is necessary because templates don't override well...
    //without this, the this.chain! call doesn't call ListPanel.chain! it calls StackPanel.chain!
    //I described this earlier
    mixin extractgen!();
    //setvarsgen! doesn't need to be here since there are no new class variables

    private Control[] chain(string _xml, T)(T viewModel) {
        Control ret[];
        foreach(vm; viewModel) {
            ret ~= BuildControl!(_xml)(vm);
        }
        return ret;
    }
}

//example of user defined xml for a new control
class DoubleText : StackPanel {
    enum _xmlstring = "
    <StackPanel name=\"doubletext type\">
        <TextBox name=\"itext1\"
            text=binding.text>
        </TextBox>
        <TextBox name=\"itext2\"
            text=\"danger zone!\">
        </TextBox>
    </StackPanel>";

    //this will call super.extract, while using the new xml
    //"Control." is necessary for this control and sometimes others because their parents overwrite extractgen! without defining a string constructor
    //in other cases you might need to rewrite extract!
    mixin Control.extractgen!(_xmlstring);
}

/*
 * THE CODE BEYOND THIS POINT IS EMBARRASSING AND SHAKY, BUT SERVES IT'S PURPOSE
 * It's for slightly picky xml parsing
 * It's not as bad as it once was...
 */

 //returns a string of code to be mixin'd, that will do the binding
 //text=\"hi\" -> this.text="hi";
 //text=method() -> this.text=method();
 //binding=something -> auto binding = something; //if not defined, auto binding = viewModel; is added. 
 //binding is always set on the first line
 //could additionally be modified to allow for method calls without assignment, but not sure if needed
pure string assignstring(string content) {
    string ret;
    int start, mid, i;
    bool tracking, pastmid, strtracker, bound;
    void falsify() {
        tracking = false;
        pastmid = false;
        strtracker = false;
    }
    pure bool isformatchar(char a) {
        return (a == ' ' || a == '\n' || a=='\t' || a=='<' || a=='>' || a=='/' || a=='\\');
    }
    string bindingconcat(string ret, string content, int start, int mid, int i) {
        string first = content[start..mid];
        string second = content[mid+1..i];
        assert(first~"="~second == content[start..i]);
        if (first == "binding") {
            bound = true;
            return "auto binding = "~second~";\n"~ret;
        } else {
            return ret~"this."~content[start..i]~";\n";
        }
    }

    for(; i < content.length; i++) {
        if (!tracking && !isformatchar(content[i])) {
            start = i;
            tracking = true;
        }
        if (tracking && content[i]=='=') {
            pastmid = true;
            mid = i;
            if (i+1 < content.length && content[i+1] == '"') {
                strtracker = true;
                i++;
            }
        }
        else if (tracking && !pastmid && isformatchar(content[i])) {
            falsify();
        }
        else if (tracking && pastmid && strtracker && content[i] == '"') {
            falsify();
            ret ~= "this."~content[start..i+1]~";\n";
        }
        else if (tracking && pastmid && !strtracker && isformatchar(content[i])) {
            falsify();
            ret = bindingconcat(ret,content,start,mid,i);
        }
    }

    if (tracking && pastmid) {
        ret = bindingconcat(ret,content,start,mid,i);
    }
    if (!bound) {
        ret = "auto binding = viewModel;\n" ~ ret;
    }

    return ret;
}
template parsexml(string xml) {
    enum parsexml = xmlparse(xml);
}
pure string[] xmlparse(string xml) {
    //tag, attr, inner, remainder, elem
    int b[5]; //"|<jkh|  |>  |</jkh|>";
    int open;
    int i = -1;

    while(++i < xml.length) {
        if (xml[i] == '<') {
            b[0] = i;
            open = 1;
            break;
        }
    }
    while(++i < xml.length) {
        if ((xml[i] == ' ' || xml[i] == '\n') && b[1] == 0) {
            b[1] = i;
        }
        if (xml[i] == '>') {
            break;
        };
        if (open == 1 && xml[i] == '/' && xml[i+1] == '>') {
            --open;
            b[3] = i;
            b[4] = i+1;
            break;
        }
    }
    b[1] = b[1] == 0 ? i : b[1];
    b[2] = i;
    while(open > 0 && ++i < xml.length-1) {
        if (xml[i] == '<') {
            open += xml[i+1]=='/' ? -1 : 1;
        }
        if (xml[i] == '/' && xml[i+1] == '>') {
            --open;
        }
    }
    b[3] = b[3] == 0 ? i : b[3];
    while (b[4] == 0 && ++i < xml.length) {
        if (xml[i] == '>') {
            break;
        }
    }
    b[4] = b[4] == 0 ? i : b[4];

    //"|<jkh|  |>  |</jkh|>";
    string ret[]; //tag, attr, inner, remainder, elem
    ret ~= xml[b[0]+1..b[1]];
    ret ~= xml[b[1]..b[2]];
    ret = ret ~ ((b[2]==b[3]) ? "" : xml[b[2]+1..b[3]]);
    ret ~= xml[b[4]+1..$];
    ret ~= xml[b[0]..b[4]+1];
    if (b[2] != b[3]) {
        assert(ret[0] == xml[b[3]+2..b[3]+(b[1]-b[0])+1]);
    }

    return ret;
}

The output when this is run is the following, which contains two runs of the control builder, to demonstrate viewModel changes. I know it's not real html, that's something I have yet to learn... sad but true.

<div bound>
<p itext1>bound inner<p>
<p itext2>bound inner inner<p>
<p itext3>my name is: robot<p>
<p itext4>too deep, would error if I tried binding<p>
<div somename>
<p itextinside>first<p>
<p itextinside>second<p>
<p itextinside>third<p>
<div>
<div double>
<p itext1>bound inner inner<p>
<p itext2>danger zone!<p>
<div>
<div>

<div new text>
<p itext1>new inner text<p>
<p itext2>bound inner inner<p>
<p itext3>my name is: robot<p>
<p itext4>too deep, would error if I tried binding<p>
<div somename>
<p itextinside>first<p>
<p itextinside>second<p>
<p itextinside>third<p>
<p itextinside>first<p>
<div>
<div double>
<p itext1>bound inner inner<p>
<p itext2>danger zone!<p>
<div>
<div>
share|improve this question
1  
You will be happy to learn that in HTML you can use either " or ' around the properties... so you can use name='item1' instead of name=\"item1\". :) You will also find this useful: developers.whatwg.org –  ANeves Mar 22 '12 at 10:39
add comment

1 Answer

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 "<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 = 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.

share|improve this answer
    
I felt this was appropriate as an answer instead of an edit because I have come up with a way to avoid the bad mixins and improve on the use of Best practices and design pattern usage –  Jean-Bernard Pellerin Feb 12 '13 at 23:17
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.