pdevito3 / craftsman

A .NET scaffolding tool to help you stop worrying about boilerplate and focus on your business logic 🚀
https://wrapt.dev
MIT License
1.12k stars 66 forks source link

Option for views #69

Closed fasteddys closed 2 years ago

fasteddys commented 2 years ago

Hello, this is fantastic, can you please allow us to scaffold basic views/tables/grid just like the default ASP MVC CRUD views

https://github.com/ZeekoZhu/TextTemplatingCore https://github.com/muhammadazam/MVC-T4-Templates/blob/master/CodeTemplates/MvcControllerWithContext/Controller.cs.t4 https://github.com/ligershark/side-waffle/tree/master/TemplatePack/ItemTemplates/Web/ASP.NET/Custom%20T4%20Files/MvcView


ModelMetadataFunctions.cs.include.t4

<#+
// Gets the related entity information for an association property where there is an associated foreign key property.
// Note: ModelMetadata.RelatedEntities contains only the related entities associated through a foreign key property.

RelatedModelMetadata GetRelatedModelMetadata(PropertyMetadata property){
    RelatedModelMetadata propertyModel;
    IDictionary<string, RelatedModelMetadata> relatedProperties;
    if(ModelMetadata.RelatedEntities != null)
    {
        relatedProperties = ModelMetadata.RelatedEntities.ToDictionary(item => item.AssociationPropertyName);
    }
    else
    {
        relatedProperties = new Dictionary<string, RelatedModelMetadata>();
    }
    relatedProperties.TryGetValue(property.PropertyName, out propertyModel);

    return propertyModel;
}

// A foreign key, e.g. CategoryID, will have an association name of Category
string GetAssociationName(PropertyMetadata property) {
    RelatedModelMetadata propertyModel = GetRelatedModelMetadata(property);
    return propertyModel != null ? propertyModel.AssociationPropertyName : property.PropertyName;
}

// A foreign key, e.g. CategoryID, will have a value expression of Category.CategoryID
string GetValueExpression(PropertyMetadata property) {
    RelatedModelMetadata propertyModel = GetRelatedModelMetadata(property);
    return propertyModel != null ? propertyModel.AssociationPropertyName + "." + propertyModel.DisplayPropertyName : property.PropertyName;
}

// This will return the primary key property name, if and only if there is exactly
// one primary key. Returns null if there is no PK, or the PK is composite.
string GetPrimaryKeyName() {
    return (ModelMetadata.PrimaryKeys != null && ModelMetadata.PrimaryKeys.Count() == 1) ? ModelMetadata.PrimaryKeys[0].PropertyName : null;
}

bool IsPropertyGuid(PropertyMetadata property) {
    return String.Equals("System.Guid", property.TypeName, StringComparison.OrdinalIgnoreCase);
}
#>

Imports.include.t4

<#@ assembly name="System.Core" #>
<#@ assembly name="System.Data.Entity" #>
<#@ assembly name="System.Data.Linq" #>
<#@ ScaffoldingAssembly Processor="ScaffoldingAssemblyLoader" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Data.Linq.Mapping" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="Microsoft.AspNet.Scaffolding.Core.Metadata" #>
<#@ parameter type="System.String" name="ViewDataTypeName" #>
<#@ parameter type="System.String" name="ViewDataTypeShortName" #>
<#@ parameter type="System.Boolean" name="IsPartialView" #>
<#@ parameter type="System.Boolean" name="IsLayoutPageSelected" #>
<#@ parameter type="System.Boolean" name="ReferenceScriptLibraries" #>
<#@ parameter type="System.Boolean" name="IsBundleConfigPresent" #>
<#@ parameter type="System.String" name="ViewName" #>
<#@ parameter type="System.String" name="LayoutPageFile" #>
<#@ parameter type="System.String" name="JQueryVersion" #>
<#@ parameter type="Microsoft.AspNet.Scaffolding.Core.Metadata.ModelMetadata" name="ModelMetadata" #>
<#@ parameter type="System.Version" name="MvcVersion" #>

View With controller context

<#@ template language="C#" HostSpecific="True" Debug="True" #>
<#@ output extension="cs" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Data.Linq" #>
<#@ ScaffoldingAssembly Processor="ScaffoldingAssemblyLoader" #>
<#
string routePrefix;
if (String.IsNullOrEmpty(AreaName)) 
{
    routePrefix = ControllerRootName;
}
else
{
    routePrefix = AreaName + "/" + ControllerRootName;
}
#>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="Microsoft.AspNet.Scaffolding.Core.Metadata" #>
<#@ parameter type="System.String" name="ControllerName" #>
<#@ parameter type="System.String" name="ControllerRootName" #>
<#@ parameter type="System.String" name="Namespace" #>
<#@ parameter type="System.String" name="AreaName" #>
<#@ parameter type="System.String" name="ContextTypeName" #>
<#@ parameter type="System.String" name="ModelTypeName" #>
<#@ parameter type="System.String" name="ModelVariable" #>
<#@ parameter type="Microsoft.AspNet.Scaffolding.Core.Metadata.ModelMetadata" name="ModelMetadata" #>
<#@ parameter type="System.String" name="EntitySetVariable" #>
<#@ parameter type="System.Boolean" name="UseAsync" #>
<#@ parameter type="System.Boolean" name="IsOverpostingProtectionRequired" #>
<#@ parameter type="System.String" name="BindAttributeIncludeText" #>
<#@ parameter type="System.String" name ="OverpostingWarningMessage" #>
<#@ parameter type="System.Collections.Generic.HashSet<System.String>" name="RequiredNamespaces" #>
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
<# if (UseAsync) { #>
using System.Threading.Tasks;
<# } #>
using System.Net;
using System.Web;
using System.Web.Mvc;
<# foreach (var namespaceName in RequiredNamespaces) { #>
using <#= namespaceName #>;
<# } #>

namespace <#= Namespace #>
{
<#
    var contextTypeName = ContextTypeName;
    var entitySetName = ModelMetadata.EntitySetName;
    var entitySetVar = EntitySetVariable ?? (String.IsNullOrEmpty(entitySetName) ? entitySetName : (entitySetName.Substring(0, length:1).ToLowerInvariant() + entitySetName.Substring(1)));
    var primaryKeyName = ModelMetadata.PrimaryKeys[0].PropertyName;
    var primaryKeyShortTypeName = ModelMetadata.PrimaryKeys[0].ShortTypeName;
    var primaryKeyDefaultValue = ModelMetadata.PrimaryKeys[0].DefaultValue;
    var primaryKeyType = ModelMetadata.PrimaryKeys[0].TypeName;
    var primaryKeyNullableTypeName = GetNullableTypeName(primaryKeyType, primaryKeyShortTypeName);
    var lambdaVar = ModelVariable[0];
    var relatedProperties = ModelMetadata.RelatedEntities.ToDictionary(item => item.AssociationPropertyName);

    string bindAttribute;
    if (IsOverpostingProtectionRequired)
    {
        bindAttribute = String.Format("[Bind(Include = \"{0}\")] ", BindAttributeIncludeText);
    }
    else
    {
        bindAttribute = String.Empty;
    }
#>
    public class <#= ControllerName #> : Controller
    {
        private <#= ContextTypeName #> db = new <#= ContextTypeName #>();

        // GET: <#= routePrefix #>
<# if (UseAsync) { #>
        public async Task<ActionResult> Index()
<# } else { #>
        public ActionResult Index(int page = 1, int pageSize = 10, string sortBy = "Id", bool isAsc = true, string search = null)
<# } #>
        {
<#  var includeExpressions = "";
        includeExpressions = String.Join("", relatedProperties.Values.Select(property => String.Format(".Include({0} => {0}.{1})", lambdaVar, property.AssociationPropertyName)));
#>
<# if(!String.IsNullOrEmpty(includeExpressions)) { #>
            var <#= entitySetVar #> = db.<#= entitySetName #><#= includeExpressions #>;

            // Search Logic
            <#= entitySetVar #> = <#= entitySetVar #>.Where(s => search == null<#  
int i=0;
var properties = ModelMetadata.Properties;
foreach (PropertyMetadata property in properties) {
    if (property.Scaffold && !property.IsPrimaryKey && !property.IsForeignKey && !property.IsComplexType) { 
        if(IsString(property.TypeName)){#> 
                || (s.<#= GetValueExpression(property) #> != null && s.<#= GetValueExpression(property) #>.ToLower().Contains(search.ToLower()))<#
        }
        else if(IsBool(property.TypeName)){#> 
                || (s.<#= GetValueExpression(property) #>.ToString().ToLower().Contains(search.ToLower()))<#
        }
        else if(HasValueExpression(property)){#> 
                //// Todo: This makes the Query slow. Need to find a better way
                //|| (s.<#= GetValueExpression(property) #> != null && s.<#= GetValueExpression(property) #>.ToString().ToLower().Contains(search.ToLower()))<#
        }
        else{#>
        <#}
    }
}#> 
            );
            // End Search Logic

            ViewBag.TotalPages = (int)Math.Ceiling((double)<#= entitySetVar #>.Count() / pageSize);
            ViewBag.CurrentPage = page;
            ViewBag.PageSize = pageSize;           
            ViewBag.Search = search;
            ViewBag.SortBy = sortBy;
            ViewBag.IsAsc = isAsc;

<#      if (UseAsync) { #>
            return View(await <#= entitySetVar #>.OrderBy(o=>o.<#= primaryKeyName #>).Skip((page - 1) * pageSize).Take(pageSize).ToListAsync());
<#      } else { #>
            return View(<#= entitySetVar #>.OrderBy(o=>o.<#= primaryKeyName #>).Skip((page - 1) * pageSize).Take(pageSize).ToList());
<#      } #>
<# } else { #>
            // Search Logic
            var <#= entitySetVar #> = db.<#= entitySetName #>.Where(s => search == null<#  
int i=0;
var properties = ModelMetadata.Properties;
foreach (PropertyMetadata property in properties) {
    if (property.Scaffold && !property.IsPrimaryKey && !property.IsForeignKey && !property.IsComplexType) { 
        if(IsString(property.TypeName)){#> 
                || (s.<#= GetValueExpression(property) #> != null && s.<#= GetValueExpression(property) #>.ToLower().Contains(search.ToLower()))<#
        }
        else if(IsBool(property.TypeName)){#> 
                || (s.<#= GetValueExpression(property) #>.ToString().ToLower().Contains(search.ToLower()))<#
        }
        else if(HasValueExpression(property)){#> 
                //// Todo: This makes the Query slow. Need to find a better way
                //|| (s.<#= GetValueExpression(property) #> != null && s.<#= GetValueExpression(property) #>.ToString().ToLower().Contains(search.ToLower()))<#
        }
        else{#>
        <#}
    }
}#> 
            );
            // End Search Logic

            ViewBag.TotalPages = (int)Math.Ceiling((double)<#= entitySetVar #>.Count() / pageSize);
            ViewBag.CurrentPage = page;
            ViewBag.PageSize = pageSize;           
            ViewBag.Search = search;
            ViewBag.SortBy = sortBy;
            ViewBag.IsAsc = isAsc;

<#      if (UseAsync) { #>
            return View(await <#= entitySetVar #><#= includeExpressions #>.OrderBy(o=>o.<#= primaryKeyName #>).Skip((page - 1) * pageSize).Take(pageSize).ToListAsync());
<#      } else { #>
            return View(<#= entitySetVar #><#= includeExpressions #>.OrderBy(o=>o.<#= primaryKeyName #>).Skip((page - 1) * pageSize).Take(pageSize).ToList());
<#      } #>
<# } #>
        }

        // GET: <#= routePrefix #>/Details/5
<# if (UseAsync) { #>
        public async Task<ActionResult> Details(<#= primaryKeyNullableTypeName #> id)
<# } else { #>
        public ActionResult Details(<#= primaryKeyNullableTypeName #> id)
<# } #>
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
<# if (UseAsync) { #>
            <#= ModelTypeName #> <#= ModelVariable #> = await db.<#= entitySetName #>.FindAsync(id);
<# } else { #>
            <#= ModelTypeName #> <#= ModelVariable #> = db.<#= entitySetName #>.Find(id);
<# } #>
            if (<#= ModelVariable #> == null)
            {
                return HttpNotFound();
            }
            return View(<#= ModelVariable #>);
        }

        // GET: <#= routePrefix #>/Create
        public ActionResult Create()
        {
<# foreach (var property in relatedProperties.Values) { #>
            ViewBag.<#= property.ForeignKeyPropertyNames[0] #> = new SelectList(db.<#= property.EntitySetName #>, "<#= property.PrimaryKeyNames[0] #>", "<#= property.DisplayPropertyName #>");
<# } #>
            return View();
        }

        // POST: <#= routePrefix #>/Create
<# if (IsOverpostingProtectionRequired) {
    foreach (var line in OverpostingWarningMessage.Split(new string[] { Environment.NewLine }, StringSplitOptions.None)) { 
#>
        // <#= line #>
<# } } #>
        [HttpPost]
        [ValidateAntiForgeryToken]
<# if (UseAsync) { #>
        public async Task<ActionResult> Create(<#= bindAttribute #><#= ModelTypeName #> <#= ModelVariable #>)
<# } else { #>
        public ActionResult Create(<#= bindAttribute #><#= ModelTypeName #> <#= ModelVariable #>)
<# } #>
        {
            if (ModelState.IsValid)
            {
<# if(!String.IsNullOrEmpty(primaryKeyType) && String.Equals("System.Guid", primaryKeyType, StringComparison.OrdinalIgnoreCase)) { #>
                <#= ModelVariable #>.<#= primaryKeyName #> = Guid.NewGuid();
<# } #>
                db.<#= entitySetName #>.Add(<#= ModelVariable #>);
<# if (UseAsync) {#>
                await db.SaveChangesAsync();
<# } else { #>
                db.SaveChanges();
<# } #>
                return RedirectToAction("Index");
            }

<# foreach (var property in relatedProperties.Values) { #>
            ViewBag.<#= property.ForeignKeyPropertyNames[0] #> = new SelectList(db.<#= property.EntitySetName #>, "<#= property.PrimaryKeyNames[0] #>", "<#= property.DisplayPropertyName #>", <#= ModelVariable #>.<#= property.ForeignKeyPropertyNames[0] #>);
<# } #>
            return View(<#= ModelVariable #>);
        }

        // GET: <#= routePrefix #>/Edit/5
<# if (UseAsync) { #>
        public async Task<ActionResult> Edit(<#= primaryKeyNullableTypeName #> id)
<# } else { #>
        public ActionResult Edit(<#= primaryKeyNullableTypeName #> id)
<# } #>
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
<# if (UseAsync) { #>
            <#= ModelTypeName #> <#= ModelVariable #> = await db.<#= entitySetName #>.FindAsync(id);
<# } else { #>
            <#= ModelTypeName #> <#= ModelVariable #> = db.<#= entitySetName #>.Find(id);
<# } #>
            if (<#= ModelVariable #> == null)
            {
                return HttpNotFound();
            }
<# foreach (var property in relatedProperties.Values) { #>
            ViewBag.<#= property.ForeignKeyPropertyNames[0] #> = new SelectList(db.<#= property.EntitySetName #>, "<#= property.PrimaryKeyNames[0] #>", "<#= property.DisplayPropertyName #>", <#= ModelVariable #>.<#= property.ForeignKeyPropertyNames[0] #>);
<# } #>
            return View(<#= ModelVariable #>);
        }

        // POST: <#= routePrefix #>/Edit/5
<# if (IsOverpostingProtectionRequired) {
    foreach (var line in OverpostingWarningMessage.Split(new string[] { Environment.NewLine }, StringSplitOptions.None)) { 
#>
        // <#= line #>
<# } } #>
        [HttpPost]
        [ValidateAntiForgeryToken]
<# if (UseAsync) { #>
        public async Task<ActionResult> Edit(<#= bindAttribute #><#= ModelTypeName #> <#= ModelVariable #>)
<# } else { #>
        public ActionResult Edit(<#= bindAttribute #><#= ModelTypeName #> <#= ModelVariable #>)
<# } #>
        {
            if (ModelState.IsValid)
            {
                db.Entry(<#= ModelVariable #>).State = EntityState.Modified;
<# if (UseAsync) { #>
                await db.SaveChangesAsync();
<# } else { #>
                db.SaveChanges();
<# } #>
                return RedirectToAction("Index");
            }
<# foreach (var property in relatedProperties.Values) { #>
            ViewBag.<#= property.ForeignKeyPropertyNames[0] #> = new SelectList(db.<#= property.EntitySetName #>, "<#= property.PrimaryKeyNames[0] #>", "<#= property.DisplayPropertyName #>", <#= ModelVariable #>.<#= property.ForeignKeyPropertyNames[0] #>);
<# } #>
            return View(<#= ModelVariable #>);
        }

        // GET: <#= routePrefix #>/Delete/5
<# if (UseAsync) { #>
        public async Task<ActionResult> Delete(<#= primaryKeyNullableTypeName #> id)
<# } else { #>
        public ActionResult Delete(<#= primaryKeyNullableTypeName #> id)
<# } #>
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
<# if (UseAsync) { #>
            <#= ModelTypeName #> <#= ModelVariable #> = await db.<#= entitySetName #>.FindAsync(id);
<# } else { #>
            <#= ModelTypeName #> <#= ModelVariable #> = db.<#= entitySetName #>.Find(id);
<# } #>
            if (<#= ModelVariable #> == null)
            {
                return HttpNotFound();
            }
            return View(<#= ModelVariable #>);
        }

        // POST: <#= routePrefix #>/Delete/5
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
<# if (UseAsync) { #>
        public async Task<ActionResult> DeleteConfirmed(<#= primaryKeyShortTypeName #> id)
<# } else { #>
        public ActionResult DeleteConfirmed(<#= primaryKeyShortTypeName #> id)
<# } #>
        {
<# if (UseAsync) { #>
            <#= ModelTypeName #> <#= ModelVariable #> = await db.<#= entitySetName #>.FindAsync(id);
<# } else { #>
            <#= ModelTypeName #> <#= ModelVariable #> = db.<#= entitySetName #>.Find(id);
<# } #>
            db.<#= entitySetName #>.Remove(<#= ModelVariable #>);
<# if (UseAsync) { #>
            await db.SaveChangesAsync();
<# } else { #>
            db.SaveChanges();
<# } #>
            return RedirectToAction("Index");
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                db.Dispose();
            }
            base.Dispose(disposing);
        }
    }
}
<#+
// This function converts the primary key short type name to its nullable equivalent when possible. This is required to make
// sure that an HTTP 400 error is thrown when the user tries to access the edit, delete, or details action with null values.
    string GetNullableTypeName(string typeName, string shortTypeName)
    {
        // The exceptions are caught because if for any reason the type is user defined, then the short type name will be used.
        // In that case the user will receive a server error if null is passed to the edit, delete, or details actions.
        Type primaryKeyType = null;
        try
        {
            primaryKeyType = Type.GetType(typeName);
        }
        catch
        {
        }
        if (primaryKeyType != null && (primaryKeyType.IsPrimitive || IsGuid(typeName)))
        {
            return shortTypeName + "?";
        }
        return shortTypeName;
    }

    bool IsGuid(string typeName) {
        return String.Equals("System.Guid", typeName, StringComparison.OrdinalIgnoreCase);
    } 

    bool IsString(string typeName) {
        return String.Equals("System.String", typeName, StringComparison.OrdinalIgnoreCase);
    } 

    bool IsDateTime(string typeName) {
        return String.Equals("System.DateTime", typeName, StringComparison.OrdinalIgnoreCase);
    }   

    bool IsDecimal(string typeName) {
        return String.Equals("System.Decimal", typeName, StringComparison.OrdinalIgnoreCase);
    }   

    bool IsInt(string typeName) {
        return String.Equals("System.Int32", typeName, StringComparison.OrdinalIgnoreCase) ||  String.Equals("System.Integer", typeName, StringComparison.OrdinalIgnoreCase) ||  String.Equals("System.Int64", typeName, StringComparison.OrdinalIgnoreCase);
    }   

    bool IsBool(string typeName) {
        return String.Equals("System.Boolean", typeName, StringComparison.OrdinalIgnoreCase);
    }

    RelatedModelMetadata GetRelatedModelMetadata(PropertyMetadata property){
        RelatedModelMetadata propertyModel;
        IDictionary<string, RelatedModelMetadata> relatedProperties;
        if(ModelMetadata.RelatedEntities != null)
        {
            relatedProperties = ModelMetadata.RelatedEntities.ToDictionary(item => item.AssociationPropertyName);
        }
        else
        {
            relatedProperties = new Dictionary<string, RelatedModelMetadata>();
        }
        relatedProperties.TryGetValue(property.PropertyName, out propertyModel);

        return propertyModel;
    }

    // A foreign key, e.g. CategoryID, will have an association name of Category
    string GetAssociationName(PropertyMetadata property) {
        RelatedModelMetadata propertyModel = GetRelatedModelMetadata(property);
        return propertyModel != null ? propertyModel.AssociationPropertyName : property.PropertyName;
    }

    // A foreign key, e.g. CategoryID, will have a value expression of Category.CategoryID
    string GetValueExpression(PropertyMetadata property) {
        RelatedModelMetadata propertyModel = GetRelatedModelMetadata(property);
        return propertyModel != null ? propertyModel.AssociationPropertyName + "." + propertyModel.DisplayPropertyName : property.PropertyName;
    }

    // A foreign key, e.g. CategoryID, will have a value expression of Category.CategoryID
    bool HasValueExpression(PropertyMetadata property) {
        RelatedModelMetadata propertyModel = GetRelatedModelMetadata(property);
        return propertyModel != null;
    }

    // This will return the primary key property name, if and only if there is exactly
    // one primary key. Returns null if there is no PK, or the PK is composite.
    string GetPrimaryKeyName() {
        return (ModelMetadata.PrimaryKeys != null && ModelMetadata.PrimaryKeys.Count() == 1) ? ModelMetadata.PrimaryKeys[0].PropertyName : null;
    }
#>
pdevito3 commented 2 years ago

Hey, I definitely want to add front end support! I actually have a react BFF coming in the next release.

As far as MVC, I'll need to think on this a bit. This would be more to support and I have less depth in MVC so I'd be hesitant to put something out that's not up to snuff for the community.

Could be up for a PR at some point but would want to strategize about it a bit. I'll look this through in more depth though and see how far this gets things.

Regardless, great idea and appreciate the input!

pdevito3 commented 2 years ago

Also, for what it's worth, you're more than welcome to fork it and add the MVC view to your own version if you'd like! I want to refs for the code to make it as friendly as possible for these instances but it's still pretty approachable as is!

fasteddys commented 2 years ago

funny thing is, I don't know react.js... because of my native asp Visual Studio use mainly, MVC asp MVC is very easy... either way any view is good I guess 👍

ASP MVC view is like handlebar templates with strongly typed models, is a lot easier to scaffold... those TT templates allow you to natively scaffold out the views and controllers, but since you already have the wep API, the views should be easier. Its a just a simple text transform

https://www.youtube.com/watch?v=kGvwOwv7KQ8

//Edit the templates https://www.youtube.com/watch?v=Y0JhoULu-zI


Other template helpers

 https://github.com/Handlebars-Net/Handlebars.Net.Helpers
 https://github.com/Handlebars-Net/Handlebars.Net

BTW - love your security helper (I think its the only one that understands security beyond RBAC)

pdevito3 commented 2 years ago

Gotcha, I'll definitely take a deeper look at the MVC stuff.

And thanks! It was really fun to work through it. If you have any improvements you come across feel free to post in there.