0

I'm looking for some sort of T4 or similar way to take C# classes and generate both the interfaces for the data that will come across the wire AND the view models in a strongly typed fashion.

I found typelite which works well for interfaces, but I'm looking for something that will handle the viewmodels too (or in addition to)

I found this project that seems excellent: Code Project View Models to JS

But of course it generates javascript and it looks non-trivial to hack in typing including things like enums, and inheritence etc. and make it work with typescript

Does anyone know of a project to do this?

If not, any tips on how to build this? I'm no T4 expert and it looks fairly impenitrible even with tools to make T4 editing less bad in Visual Studio.

Thanks!

James Hancock
  • 3,348
  • 5
  • 34
  • 59
  • 1
    T4 is really really simple. I suggest you use Tangible T4 Editor. They have a free edition, and it highlights the "template code" different from the "generated code". It's really useful. I also recommend the T4 ToolBox. I have written dozens of T4 Templates, but none are related to what you're looking for. – Federico Berasategui Oct 17 '13 at 21:32
  • Have you thought about using a client side data library like Breeze.js so you don't have to create models on the client and on the server? – PW Kad Oct 17 '13 at 21:37
  • 1
    The problem with breeze is that it still isn't strongly typed. The entire intent is to have absolutely everything strongly typed throughout so any change anywhere will throw compile time errors instead of things randomly showing up. By using a t4 viewmodel generator, even if you're using Breeze you can strongly cast your objects so that everything is always strongly typed in Typescript. – James Hancock Oct 17 '13 at 21:39
  • As for Tangible, ya, that's what I'm thinking about doing. Just been messing with it, and it's just pretty ugly documentation on doing something as simple as looping through everything in a class library and looking for an attribute, and then I need to do things like create enumerations, remember that I've created them, handle inheritance etc. Trying to be lazy and not reinvent the wheel. – James Hancock Oct 17 '13 at 21:41
  • "Trying to be lazy and not reinvent the wheel." - Sometimes you have to invent a different wheel in order to make it fit to your business requirements (you already noticed the Breeze.js wheel doesn't really fit). – Just another metaprogrammer Oct 18 '13 at 13:38
  • See my answer, it has some helper methods that get you further. – Anders Oct 18 '13 at 14:07
  • You should have a look at http://njsonschema.org/: The projects can be used to convert C# classes into TypeScript classes (also supports KnockoutJS observable classes) – Rico Suter Jun 04 '16 at 15:46

1 Answers1

3

I did this T4 to make my DTO contracts available from javascript, maybe it can help you

<#@ template debug="true" hostSpecific="true" #>
<#@ output extension=".js" #>
<#@ Assembly Name="System.Core" #>
<#@ Assembly Name="System.Windows.Forms" #>
<#@ import namespace="System" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="EnvDTE" #>
<#@ include file="..\T4\Automation.ttinclude"#><#
var project = VisualStudioHelper.GetProject("MyApp.Core.Contracts");      
var contracts = GetSubClasses("MyApp.Core.Contracts.Commands.Command", project)
    .Concat(GetSubClasses("MyApp.Core.Contracts.Queries.Query", project));

#>(function(MyApp) {
    function buildContract(contract) {
        return { type: contract.constructor.type, data: ko.toJSON(contract) };
    }
    var url = "api/commandQuery";
    MyApp.cqrs = {
        sendQuery: function(query, callback) {
            $.getJSON(url, buildContract(query), callback);
        },
        sendCommand: function(command) {
            MyApp.utils.post(url, buildContract(command));
        }
    };
<#


foreach(var contract in contracts) {
        #>  
    <#
    foreach(var part in BuildNameSpace(contract)) {
        #><#= part #>
    <#
    }

    var properties = GetProperties(contract).Select(p => CamelCased(p.Name)).ToList();
    var args = string.Join(", ", properties);

    #>

    window.<#= contract.FullName #> = function(<#= args #>) {<#
    foreach(var property in properties) {#>

        this.<#= property #> = <#= property #>;<#
    }
    #>

    };
    window.<#= contract.FullName #>.type = "<#= contract.FullName #>";
<#
}
#>
})(window.MyApp = window.MyApp || {});
<#+

private static IEnumerable<string> BuildNameSpace(CodeClass @class)
{
    return BuildNameSpace(@class.Namespace.Name.Split('.'), "window", new List<string>());
}            

private static IEnumerable<string> BuildNameSpace(IEnumerable<string> @namespace, string parent, List<string> parts)
{
    var part = @namespace.FirstOrDefault();
    if (part == null) return parts;

    var current = string.Format("{0}.{1}", parent, part);
    parts.Add(string.Format("{0} = ({0} || {{}});", current));
    return BuildNameSpace(@namespace.Skip(1), current, parts);
}

public IEnumerable<CodeClass> GetSubClasses(string baseClass, Project project)
{
    return VisualStudioHelper       
        .CodeModel
        .GetAllCodeElementsOfType(project.CodeModel.CodeElements, EnvDTE.vsCMElement.vsCMElementClass, false)
        .Cast<CodeClass>()
        .Where(c => GetInheritance(c).Any(b => b.FullName == baseClass) && !c.IsAbstract)
        .ToList(); 
}


public IEnumerable<CodeClass> GetInheritance(CodeClass @class) 
{
    return GetInheritance(@class, new List<CodeClass>());
}

public IEnumerable<CodeClass> GetInheritance(CodeClass @class, List<CodeClass> collection) 
{
    foreach(CodeClass @base in @class.Bases) 
    {
        collection.Add(@base);
        GetInheritance(@base, collection);
    }

    return collection;
}

public string CamelCased(string pascalCased) {
    return pascalCased.Substring(0, 1).ToLower() + pascalCased.Substring(1);
}

public IEnumerable<CodeProperty> GetProperties(CodeClass @class)
{
    if (@class == null) 
        return new List<CodeProperty>();

    var baseProperties = GetProperties(@class.Bases.Cast<CodeClass>().FirstOrDefault());

    return baseProperties.Concat(@class
        .Members
        .Cast<CodeElement>()
        .Where(ce => ce.Kind == vsCMElement.vsCMElementProperty)
        .Cast<CodeProperty>()
        .Where(p => p.Access == vsCMAccess.vsCMAccessPublic));
    }
 #>

It outputs a js looking like this

(function(MyApp) {
    function buildContract(contract) {
        return { type: contract.constructor.type, data: ko.toJSON(contract) };
    }
    var url = "api/commandQuery";
    MyApp.cqrs = {
        sendQuery: function(query, callback) {
            $.getJSON(url, buildContract(query), callback);
        },
        sendCommand: function(command) {
            MyApp.utils.post(url, buildContract(command));
        }
    };

    window.MyApp = (window.MyApp || {});
    window.MyApp.Core = (window.MyApp.Core || {});
    window.MyApp.Core.Contracts = (window.MyApp.Core.Contracts || {});
    window.MyApp.Core.Contracts.Commands = (window.MyApp.Core.Contracts.Commands || {});

    window.MyApp.Core.Contracts.Commands.FooCommand = function(bar) {
        this.bar = bar;
    };
    window.MyApp.Core.Contracts.Commands.FooCommand.type = "MyApp.Core.Contracts.Commands.FooCommand";
})(window.MyApp = window.MyApp || {});

Note that it has a dependency to a Tangible T4 Editor template

<#@ include file="..\T4\Automation.ttinclude"#>
Anders
  • 17,306
  • 10
  • 76
  • 144