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>
"or'around the properties... so you can usename='item1'instead ofname=\"item1\". :) You will also find this useful: developers.whatwg.org – ANeves Mar 22 '12 at 10:39