Volta - Type reuse across tiers
So my last post was a quick look at Volta & WinForms (if you wonder why I was using WinForms rather then web, I just figured it'd be a good place to start, without the complications of what can/can not be translated to javascript) - I haven't really been keeping track of what other people are doing with Volta, but there have been a couple of posts from Microsoft bloggers, including Wes Dyer (on tier splitting a web application) and Drargos Manolescu (on tier splitting a winforms application) - hopefully some of the
other members on the Volta team will pick up the gauntlet and start blogging shortly too.
This post is going to be very short (if you ignore the code snippets) - we've already seen how classes can be split into two, one being the client proxy the other the service implementation - but what happens if a class is used in the client and server but isn't fixed/pinned to run on a specific Tier via the RunAt attribute... that's the focus of this post.
So, in this example - I'm going to add a helper class to my previous example from the last
post - which just formats some text all "pretty" like:
public class HelperClass
{
public string PrettyFormat(string text)
{
return string.Format("--> {0} <--",>--",>
}
}
Now I'll update my service to use pretty formatting:
[RunAt("Server")]
public class SomeService
{
private readonly HelperClass _helper = new HelperClass();public string WelcomeMessage(string name)
{
return _helper.PrettyFormat(string.Format("Welcome: {0}", name));
}
}
And lastly, I'll add an additional button called "welcomeLocalButton" which when clicked will make use of the helper to format some text on the client, instead of calling the WelcomeMessage method on the service.
public partial class Form1 : Form
{
SomeService service = new SomeService();
private HelperClass helper = new HelperClass();public Form1()
{
InitializeComponent();
}private void welcomeButton_Click(object sender, EventArgs e)
{
welcomeOutput.AppendText(service.WelcomeMessage(nameTextBox.Text));
}private void welcomeLocalButton_Click(object sender, EventArgs e)
{
welcomeOutput.AppendText(helper.PrettyFormat("Welcome Local " + nameTextBox.Text));
}
}
So this is how the app looks now...
Compile and run - everything works as expected, now looking at the generated client and server assemblies we see both contain... the same class - yep that's right it just duplicates the class in both the client and server tiers, without changing it at all.
Now for windows forms this doesn't appear as all that much of a miracle (it's still nice, we wouldn't want helper classes being turned into implicit services just because they're used across tiers)... but for web or other target platforms (such as embedded
devices) this is where the elegance begins to kick in, not only are you able to declaratively specify where code is executing, but if you don't specify/fix (I wonder what the correct terminology is here?) a type to a specific tier you get the best of both worlds i.e. code that's native assemblies on the origin/server side and javascript implementations on the client side.
So to demonstrate the web equivalent I whipped up the following example - I was trying to think of something interesting that would be useful to do client and server-side - and thought, perhaps I would use Andrew's Inflector.Net - sadly there are a few issues with that, namely Regex isn't supported out of the box in Volta (which is a real shame, this is something I would've have expected to have been in the preview - regex being such a swiss army knife, especially for scripting) - so instead I picked something a little simpler - the Ordinalize functionality from Inflector, and just stripped out the other unrequired methods which would give us grief.
So the example is pretty simple, we have a web page that looks like this:
You enter a number, it get's ordinalized, either via a remote call or a client-side call... so let's take a quick look at the code, first we have the ordinalize method:
public static class Inflector
{
public static string Ordinalize(string number)
{
int n = int.Parse(number);
int nMod100 = n % 100;if (nMod100 >= 11 && nMod100 <=>=>
{
return number + "th";
}switch (n % 10)
{
case 1:
return number + "st";
case 2:
return number + "nd";
case 3:
return number + "rd";
default:
return number + "th";
}
}
}
Then we have the server-side OrdinalizerService:
[RunAtOrigin]
public class OrdinalizerService
{
public string Ordinalize(string number)
{
return Inflector.Ordinalize(number);
}[Async]
public extern void Ordinalize(string number, Callbackcallback);
}
And finally we have the UI code:
public partial class VoltaPage1 : Page
{
Input numberElement;
Button button1;
Div resultsElement;
Button button2;public VoltaPage1()
{
InitializeComponent();var ordinalizer = new OrdinalizerService();
button1.Click += delegate
{
var name = numberElement.Value;
resultsElement.InnerText = Inflector.Ordinalize(numberElement.Value);
};button2.Click += delegate
{
var name = numberElement.Value;
ordinalizer.Ordinalize(
name,
message => { resultsElement.InnerText = "Remote: " + message; });
};
}partial void InitializeComponent()
{
numberElement = Document.GetById("Text1");
resultsElement = Document.GetById("Results");
button1 = Document.GetById
So... what's left after the tier-split?
Well if you open up reflector and look at either the client or the server assembly - you will still see the same Inflector class i.e. just like the previous winforms example it just copies it to both tiers - and that's it... no magic at this point, because the process of transforming the class to javascript doesn't occur untill runtime. Though obviously it does need to copy it because the client and service layer are entirely independent.
I could stop there - but I'm sure many people are curious as to just what the javascript looks like :)
So just to round this post out - we'll take a brief look at the generated javascript - so if you use a tool like firebug while loading a page developed with volta you will see alot of activity going on as individual types are loaded one by one from the server, like so...
There are pages and pages of these calls... though there are plans to reduce the number of round trips in the future (obviously a round trip per type is a pretty bad idea in a complex app) but you need to keep your eye on the prize... Volta is not competing with meticulously hand crafted MVC web-based solutions with little sprinkles of Ajax here and there... the benefits would come from employing it where the complexity and drudgery of client side
scripting is overwhelming and large amounts of asynchronous messages are being exchanged between client and server... at least that's where I see a sweet spot... obviously there are plenty of other side-effects as well (ubiquitous refactoring springs to mind).
Now if you scroll down past all the standard (BCL) and volta related types you eventually find... no mention of the Inflector type.
No magic here though - just type a value into the textbox and click Ordinalize Local - and flick back to Firebug's console... and you will see a request being made for the Inflector type - types are lazy loaded - makes a lot of sense, when you might have types only required by a single UI element on the screen that the user never touches.
So here's the request for the inflector type:
Notice the two query string parameters a (assembly) and t (type) ... and now if we were to flick over to the response tab, we would see the javascript produced for that type (see below)
Notice it adds this type to the list of types in the assembly, maintaining the same symantecs between javascript and the .net framework - obviously the javascript is a little scary, especially considering all the existing variable names have been lost - but for all that it's quite readable:
var CurrentAssembly = Assemblies["VoltaWeb.Client, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"];CurrentAssembly.TypeDefs["VoltaWeb.Inflector"] = (function(){
var Ret = {};var Assembly = CurrentAssembly;
var asmRef_0 = Assembly.References.ref_0();
var typDef_0 = GetTypeDef(asmRef_0, "System.Object");
var typDef_1 = GetTypeDef(asmRef_0, "System.Object");
var typRef_0 = GetTypeRef(asmRef_0, "System.String");
var typRef_1 = GetTypeRef(Assembly, "VoltaWeb.Inflector");
var typRef_2 = GetTypeRef(asmRef_0, "System.Object");
var typRef_3 = GetTypeRef(asmRef_0, "System.Object");
var typRef_4 = GetTypeRef(asmRef_0, "System.Boolean");
var typRef_5 = GetTypeRef(asmRef_0, "System.Int32");
var typRef_6 = GetTypeRef(asmRef_0, "System.Void");
var methDef_0 = GetMethodDef(typDef_0, "ToString", [typRef_0]);
var methDef_1 = GetMethodDef(typDef_1, "Equals", [typRef_2, typRef_4]);
var methDef_2 = GetMethodDef(typDef_1, "GetHashCode", [typRef_5]);
var methDef_3 = GetMethodDef(typDef_0, "Finalize", [typRef_6]);
var methRef_0 = GetMethodRef(typRef_3, "ToString", [typRef_0]);
var methRef_1 = GetMethodRef(typRef_3, "Equals", [typRef_2, typRef_4]);
var methRef_2 = GetMethodRef(typRef_3, "GetHashCode", [typRef_5]);
var methRef_3 = GetMethodRef(typRef_3, "Finalize", [typRef_6]);
var Methods = {};Methods["meth_13"/*VoltaWeb.Inflector.Ordinalize*/] = function _VoltaWeb_Inflector_Ordinalize_System_String_(param_2) {
var asmRef_0 = Assembly.References.ref_0();
var typDef_0 = GetTypeDef(asmRef_0, "System.Int32");
var typDef_1 = GetTypeDef(asmRef_0, "System.String");
var typRef_0 = GetTypeRef(asmRef_0, "System.String");
var typRef_1 = GetTypeRef(asmRef_0, "System.Int32");
var methDef_0 = GetMethodDef(typDef_0, "Parse", [typRef_0, typRef_1]);
var methDef_1 = GetMethodDef(typDef_1, "Concat", [typRef_0, typRef_0, typRef_0]);
var loc_3 = typDef_0.Initializer({});
var loc_4 = typDef_0.Initializer({});
var loc_5 = typDef_0.Initializer({});
var $next;
$next = 0;while (true) switch($next) {
case 0:
{
loc_3 = methDef_0(param_2)/*System.Int32.Parse(System.String)*/;
loc_4 = loc_3 % 100;
var br1 = loc_4 <>if (br1 || br1 === "") {
$next = 40;
continue;
}var br2 = loc_4 > 13;
if (br2 || br2 === "") {
$next = 40;
continue;
}
return methDef_1(param_2, "th")/*System.String.Concat(System.String,System.String)*/;
$next = 40;
}
case 40:
{
loc_5 = loc_3 % 10;switch(loc_5 - 1){
case 0:
$next = 70;
continue;
case 1:
$next = 82;
continue;
case 2:
$next = 94;
continue;
}
$next = 106;
continue;
$next = 70;
}
case 70:
{
return methDef_1(param_2, "st")
/*System.String.Concat(System.String,System.String)*/;
$next = 82;
}
case 82:
{
return methDef_1(param_2, "nd")
/*System.String.Concat(System.String,System.String)*/;
$next = 94;
}
case 94:
{
return methDef_1(param_2, "rd")/*System.String.Concat(System.String,System.String)*/;
$next = 106;
}
case 106:
{
return methDef_1(param_2, "th")
/*System.String.Concat(System.String,System.String)*/;
}
}
};Ret["Methods"] = Methods;
var VTable = {};
VTable[methRef_0] = methDef_0;
VTable[methRef_1] = methDef_1;
VTable[methRef_2] = methDef_2;
VTable[methRef_3] = methDef_3;
Ret["VTable"] = VTable;
var Parents = {};
Parents[typRef_1.Id] = true;
Parents[typRef_2.Id] = true;
Ret["Parents"] = Parents;
Ret["PublicMethods"] = {};
Ret["PublicMethods"]["Ordinalize"] = {};
Ret["PublicMethods"]["Ordinalize"][GetSignature([typRef_0, typRef_0])] = "meth_13";
/*VoltaWeb.Inflector.Ordinalize*/
Ret["Assembly"] = CurrentAssembly;
Ret["Name"] = "VoltaWeb.Inflector";
Ret["Initializer"] = (function(instance){
return instance;
});
Ret["TypeInitializer"] = (function(_vT){});
return Ret;
})();
Now the above output was generated because I had enabled "verbose javascript output" - by default this isn't on, but can be enabled via a check box on the Volta tab, in the properties for the Volta project:
For those people who care about their javascript being compacted, this is what the non-verbose equivalent looked like, which has no comments or unnecessary whitespace - though with all the qualified type name strings sprinkled everywhere I think the javascript's always going to be a little weighty, and I do wonder if perhaps they couldn't eliminate the need for a lot of them in the non-verbose javascript via some variables being introduced at the top - just look how many times the "System.String" literal is sprinkled around in this small class.
var CurrentAssembly = Assemblies["VoltaPrelude, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"];CurrentAssembly.TypeDefs["Microsoft.LiveLabs.Volta.JavaScript.Global"] = (function(){var Ret = {};var Assembly = CurrentAssembly;var oA = Assembly.References.cA();var kA = GetTypeDef(oA, "System.Object");var kB = GetTypeDef(oA, "System.Object");var lA = GetTypeRef(oA, "System.String");var lB = GetTypeRef(Assembly, "Microsoft.LiveLabs.Volta.JavaScript.Object");var lC = GetTypeRef(TypeParameters, "T");var lD = GetTypeRef(oA, "System.Double");var lE = GetTypeRef(oA, "System.Boolean");var lF = GetTypeRef(oA, "System.Int32");var lG = GetTypeRef(Assembly, "Microsoft.LiveLabs.Volta.JavaScript.Function");var lH = GetTypeRef(Assembly, "Microsoft.LiveLabs.Volta.JavaScript.Arguments");var lI = GetTypeRef(Assembly, "Microsoft.LiveLabs.Volta.JavaScript.Global");var lJ = GetTypeRef(oA, "System.Object");var lK = GetTypeRef(oA, "System.Object");var lL = GetTypeRef(oA, "System.Void");var mA = GetMethodDef(kA, "ToString", [lA]);var mB = GetMethodDef(kB, "Equals", [lJ, lE]);var mC = GetMethodDef(kB, "GetHashCode", [lF]);var mD = GetMethodDef(kA, "Finalize", [lL]);var nA = GetMethodRef(lK, "ToString", [lA]);var nB = GetMethodRef(lK, "Equals", [lJ, lE]);var nC = GetMethodRef(lK, "GetHashCode", [lF]);var nD = GetMethodRef(lK, "Finalize", [lL]);var Methods = {};Methods["eEI"] = function(iDV){
var rv = decodeURI(iDV);if (rv != null && rv._vT == null) {
rv._vT = GetTypeDef(Assembly.References.cA(), "System.String");}return rv;};Methods["eEJ"] = function(iDW){
var rv = decodeURIComponent(iDW);if (rv != null && rv._vT == null) {
rv._vT = GetTypeDef(Assembly.References.cA(), "System.String");}return rv;};Methods["eEK"] = function(iDX){
var rv = encodeURI(iDX);if (rv != null && rv._vT == null) {
rv._vT = GetTypeDef(Assembly.References.cA(), "System.String");}return rv;};Methods["eEL"] = function(iDY){
var rv = encodeURIComponent(iDY);if (rv != null && rv._vT == null) {
rv._vT = GetTypeDef(Assembly.References.cA(), "System.String");}return rv;};Methods["eEM"] = function(iDZ){
var rv = escape(iDZ);if (rv != null && rv._vT == null) {
rv._vT = GetTypeDef(Assembly.References.cA(), "System.String");}return rv;};Methods["eEN"] = function(iD0){
var rv = (function(code){
eval("var temp = " + code);return temp;})(iD0);if (rv != null && rv._vT == null) {
rv._vT = GetTypeDef(Assembly, "Microsoft.LiveLabs.Volta.JavaScript.Object");}return rv;};Methods["eEO"] = (function(dB){
return function(iD1){
var rv = (function(code){
eval("var temp = " + code);return temp;})(iD1);if (rv != null && rv._vT == null) {
rv._vT = dB;}return rv;};});Methods["eEP"] = function(){
var rv = Infinity;if (rv != null && rv._vT == null) {
rv._vT = GetTypeDef(Assembly.References.cA(), "System.Double");}return rv;};Methods["eEQ"] = function(iD2){
var rv = isFinite(iD2);if (rv != null && rv._vT == null) {
rv._vT = GetTypeDef(Assembly.References.cA(), "System.Boolean");}return rv;};Methods["eER"] = function(iD3){
var rv = isNaN(iD3);if (rv != null && rv._vT == null) {
rv._vT = GetTypeDef(Assembly.References.cA(), "System.Boolean");}return rv;};Methods["eES"] = function(){
var rv = NaN;if (rv != null && rv._vT == null) {
rv._vT = GetTypeDef(Assembly.References.cA(), "System.Double");}return rv;};Methods["eET"] = function(iD4){
var rv = parseFloat(iD4);if (rv != null && rv._vT == null) {
rv._vT = GetTypeDef(Assembly.References.cA(), "System.Double");}return rv;};Methods["eEU"] = function(iD5){
var rv = parseInt(iD5);if (rv != null && rv._vT == null) {
rv._vT = GetTypeDef(Assembly.References.cA(), "System.Double");}return rv;};Methods["eEV"] = function(iD6, iD7){
var rv = parseInt(iD6, iD7);if (rv != null && rv._vT == null) {
rv._vT = GetTypeDef(Assembly.References.cA(), "System.Double");}return rv;};Methods["eEW"] = function(iD8){
var rv = (function(item){
return typeof item;})(iD8);if (rv != null && rv._vT == null) {
rv._vT = GetTypeDef(Assembly.References.cA(), "System.String");}return rv;};Methods["eEX"] = function(iD9, iEA){
var rv = (function(item, typeConstructor){
return item instanceof typeConstructor;})(iD9, iEA);if (rv != null && rv._vT == null) {
rv._vT = GetTypeDef(Assembly.References.cA(), "System.Boolean");}return rv;};Methods["eEY"] = function(iEB, iEC){
var gE;var $next;$next = 0;while (true) switch($next){case 0:
{
gE = GetTypeDef(Assembly, "Microsoft.LiveLabs.Volta.JavaScript.Function").Methods.eED(iEC);var br1 = gE;if (br1 || br1 === "") {
$next = 12;continue; }return 0;$next = 12;}case 12:
{
return Methods.eEX(iEB, gE);}}};Methods["eEZ"] = function(){
var rv = undefined;if (rv != null && rv._vT == null) {
rv._vT = GetTypeDef(Assembly, "Microsoft.LiveLabs.Volta.JavaScript.Object");}return rv;};Methods["eE0"] = function(iED){
var rv = unescape(iED);if (rv != null && rv._vT == null) {
rv._vT = GetTypeDef(Assembly.References.cA(), "System.String");}return rv;};Methods["eE1"] = function(){
var rv = arguments;if (rv != null && rv._vT == null) {
rv._vT = GetTypeDef(Assembly, "Microsoft.LiveLabs.Volta.JavaScript.Arguments");}return rv;};Ret["Methods"] = Methods;var VTable = {};VTable[nA] = mA;VTable[nB] = mB;VTable[nC] = mC;VTable[nD] = mD;Ret["VTable"] = VTable;var Parents = {};Parents[lI.Id] = true;Parents[lJ.Id] = true;Ret["Parents"] = Parents;Ret["PublicMethods"] = {};Ret["PublicMethods"]["DecodeURI"] = {};Ret["PublicMethods"]["DecodeURI"][GetSignature([lA, lA])] = "eEI";Ret["PublicMethods"]["DecodeURIComponent"] = {};Ret["PublicMethods"]["DecodeURIComponent"][GetSignature([lA, lA])] = "eEJ";Ret["PublicMethods"]["EncodeURI"] = {};Ret["PublicMethods"]["EncodeURI"][GetSignature([lA, lA])] = "eEK";Ret["PublicMethods"]["EncodeURIComponent"] = {};Ret["PublicMethods"]["EncodeURIComponent"][GetSignature([lA, lA])] = "eEL";Ret["PublicMethods"]["Escape"] = {};Ret["PublicMethods"]["Escape"][GetSignature([lA, lA])] = "eEM";Ret["PublicMethods"]["Eval"] = {};Ret["PublicMethods"]["Eval"][GetSignature([lA, lB])] = "eEN";Ret["PublicMethods"]["Eval"][GetSignature([lA, lC])] = "eEO";Ret["PublicMethods"]["get_Infinity"] = {};Ret["PublicMethods"]["get_Infinity"][GetSignature([lD])] = "eEP";Ret["PublicMethods"]["IsFinite"] = {};Ret["PublicMethods"]["IsFinite"][GetSignature([lD, lE])] = "eEQ";Ret["PublicMethods"]["IsNaN"] = {};Ret["PublicMethods"]["IsNaN"][GetSignature([lD, lE])] = "eER";Ret["PublicMethods"]["get_NaN"] = {};Ret["PublicMethods"]["get_NaN"][GetSignature([lD])] = "eES";Ret["PublicMethods"]["ParseFloat"] = {};Ret["PublicMethods"]["ParseFloat"][GetSignature([lA, lD])] = "eET";Ret["PublicMethods"]["ParseInt"] = {};Ret["PublicMethods"]["ParseInt"][GetSignature([lA, lD])] = "eEU";Ret["PublicMethods"]["ParseInt"][GetSignature([lA, lF, lD])] = "eEV";Ret["PublicMethods"]["TypeOf"] = {};Ret["PublicMethods"]["TypeOf"][GetSignature([lB, lA])] = "eEW";Ret["PublicMethods"]["InstanceOf"] = {};Ret["PublicMethods"]["InstanceOf"][GetSignature([lB, lG, lE])] = "eEX";Ret["PublicMethods"]["InstanceOf"][GetSignature([lB, lA, lE])] = "eEY";Ret["PublicMethods"]["get_Undefined"] = {};Ret["PublicMethods"]["get_Undefined"][GetSignature([lB])] = "eEZ";Ret["PublicMethods"]["Unescape"] = {};Ret["PublicMethods"]["Unescape"][GetSignature([lA, lA])] = "eE0";Ret["PublicMethods"]["get_Arguments"] = {};Ret["PublicMethods"]["get_Arguments"][GetSignature([lH])] = "eE1";Ret["Assembly"] = CurrentAssembly;Ret["Name"] = "Microsoft.LiveLabs.Volta.JavaScript.Global";Ret["Initializer"] = (function(instance){
return instance;});Ret["TypeInitializer"] = (function(_vT){
});return Ret;})();
At any rate I'm going to wrap it up there for now... work to do, but I have to say for a very early preview Volta is proving surprisingly robust - I really hope in the long term it becomes a product in and of itself, rather then being reabsorbed - to me it's a logical approach to reducing complexity in a lot of what we do today - especially when it comes to embracing the DRY principal.