├── .bowerrc ├── .gitignore ├── Controllers └── HomeController.cs ├── DynamicFormTagHelper.csproj ├── DynamicFormTagHelper.sln ├── LICENSE ├── Models └── PersonViewModel.cs ├── Program.cs ├── Properties └── launchSettings.json ├── README.md ├── Startup.cs ├── TagHelpers ├── AutoCompleteAttribute.cs ├── DynamicFormTagHelper.cs ├── FormGroupBuilder.cs └── ItemsSourceAttribute.cs ├── Views ├── Home │ ├── About.cshtml │ ├── Contact.cshtml │ ├── Index.cshtml │ └── Show.cshtml ├── Shared │ ├── EditorTemplates │ │ └── DateTime.cshtml │ ├── Error.cshtml │ ├── _Layout.cshtml │ └── _ValidationScriptsPartial.cshtml ├── _ViewImports.cshtml └── _ViewStart.cshtml ├── appsettings.json ├── bower.json ├── gulpfile.js ├── package.json ├── web.config └── wwwroot ├── css └── site.css ├── favicon.ico ├── images ├── banner1.svg ├── banner2.svg ├── banner3.svg └── banner4.svg └── js └── site.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "wwwroot/lib" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | wwwroot/lib 29 | wwwroot/lib/* 30 | wwwroot/lib/**/* 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # DNX 46 | project.lock.json 47 | artifacts/ 48 | 49 | *_i.c 50 | *_p.c 51 | *_i.h 52 | *.ilk 53 | *.meta 54 | *.obj 55 | *.pch 56 | *.pdb 57 | *.pgc 58 | *.pgd 59 | *.rsp 60 | *.sbr 61 | *.tlb 62 | *.tli 63 | *.tlh 64 | *.tmp 65 | *.tmp_proj 66 | *.log 67 | *.vspscc 68 | *.vssscc 69 | .builds 70 | *.pidb 71 | *.svclog 72 | *.scc 73 | 74 | # Chutzpah Test files 75 | _Chutzpah* 76 | 77 | # Visual C++ cache files 78 | ipch/ 79 | *.aps 80 | *.ncb 81 | *.opendb 82 | *.opensdf 83 | *.sdf 84 | *.cachefile 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # NuGet Packages 149 | *.nupkg 150 | # The packages folder can be ignored because of Package Restore 151 | **/packages/* 152 | # except build/, which is used as an MSBuild target. 153 | !**/packages/build/ 154 | # Uncomment if necessary however generally it will be regenerated when needed 155 | #!**/packages/repositories.config 156 | 157 | # Microsoft Azure Build Output 158 | csx/ 159 | *.build.csdef 160 | 161 | # Microsoft Azure Emulator 162 | ecf/ 163 | rcf/ 164 | 165 | # Microsoft Azure ApplicationInsights config file 166 | ApplicationInsights.config 167 | 168 | # Windows Store app package directory 169 | AppPackages/ 170 | BundleArtifacts/ 171 | 172 | # Visual Studio cache files 173 | # files ending in .cache can be ignored 174 | *.[Cc]ache 175 | # but keep track of directories ending in .cache 176 | !*.[Cc]ache/ 177 | 178 | # Others 179 | ClientBin/ 180 | ~$* 181 | *~ 182 | *.dbmdl 183 | *.dbproj.schemaview 184 | *.pfx 185 | *.publishsettings 186 | node_modules/ 187 | orleans.codegen.cs 188 | 189 | # RIA/Silverlight projects 190 | Generated_Code/ 191 | 192 | # Backup & report files from converting an old project file 193 | # to a newer Visual Studio version. Backup files are not needed, 194 | # because we have git ;-) 195 | _UpgradeReport_Files/ 196 | Backup*/ 197 | UpgradeLog*.XML 198 | UpgradeLog*.htm 199 | 200 | # SQL Server files 201 | *.mdf 202 | *.ldf 203 | 204 | # Business Intelligence projects 205 | *.rdl.data 206 | *.bim.layout 207 | *.bim_*.settings 208 | 209 | # Microsoft Fakes 210 | FakesAssemblies/ 211 | 212 | # GhostDoc plugin setting file 213 | *.GhostDoc.xml 214 | 215 | # Node.js Tools for Visual Studio 216 | .ntvs_analysis.dat 217 | 218 | # Visual Studio 6 build log 219 | *.plg 220 | 221 | # Visual Studio 6 workspace options file 222 | *.opt 223 | 224 | # Visual Studio LightSwitch build output 225 | **/*.HTMLClient/GeneratedArtifacts 226 | **/*.DesktopClient/GeneratedArtifacts 227 | **/*.DesktopClient/ModelManifest.xml 228 | **/*.Server/GeneratedArtifacts 229 | **/*.Server/ModelManifest.xml 230 | _Pvt_Extensions 231 | 232 | # Paket dependency manager 233 | .paket/paket.exe 234 | 235 | # FAKE - F# Make 236 | .fake/ 237 | -------------------------------------------------------------------------------- /Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using DynamicFormTagHelper.Models; 2 | using Microsoft.AspNetCore.Mvc; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace WebApplication.Controllers 7 | { 8 | public class HomeController : Controller 9 | { 10 | public IActionResult Index() 11 | { 12 | return View(new PersonViewModel()); 13 | } 14 | 15 | [HttpPost] 16 | public IActionResult Index(PersonViewModel model) 17 | { 18 | if (ModelState.IsValid) 19 | { 20 | return Show(model); 21 | } 22 | return View(model); 23 | } 24 | 25 | public IActionResult Show(PersonViewModel model) 26 | { 27 | return View("Show", model); 28 | } 29 | 30 | [HttpGet] 31 | public IActionResult DogNames(string typed) 32 | { 33 | var names = new List() 34 | { 35 | "DaDawg", 36 | "Dawg", 37 | "Rex", 38 | "Kablam", 39 | "Fenton", 40 | "Fenta" 41 | }; 42 | var result = names.Where(n => n.ToUpper().Contains(typed.ToUpper())).ToList(); 43 | return Json(result); 44 | } 45 | 46 | public IActionResult About() 47 | { 48 | ViewData["Message"] = "Your application description page."; 49 | 50 | return View(); 51 | } 52 | 53 | public IActionResult Contact() 54 | { 55 | ViewData["Message"] = "Your contact page."; 56 | 57 | return View(); 58 | } 59 | 60 | public IActionResult Error() 61 | { 62 | return View(); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /DynamicFormTagHelper.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp1.1 5 | portable 6 | true 7 | DynamicFormTagHelper 8 | Exe 9 | DynamicFormTagHelper 10 | 1.1.1 11 | $(PackageTargetFallback);dotnet5.6;dnxcore50;portable-net45+win8 12 | 13 | 14 | 15 | 16 | PreserveNewest 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | All 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /DynamicFormTagHelper.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 15 3 | VisualStudioVersion = 15.0.26228.4 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DynamicFormTagHelper", "DynamicFormTagHelper.csproj", "{D8A9958A-7EC2-4C4B-ADF8-02886CECD41F}" 6 | EndProject 7 | Global 8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 9 | Debug|Any CPU = Debug|Any CPU 10 | Release|Any CPU = Release|Any CPU 11 | EndGlobalSection 12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 13 | {D8A9958A-7EC2-4C4B-ADF8-02886CECD41F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 14 | {D8A9958A-7EC2-4C4B-ADF8-02886CECD41F}.Debug|Any CPU.Build.0 = Debug|Any CPU 15 | {D8A9958A-7EC2-4C4B-ADF8-02886CECD41F}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | {D8A9958A-7EC2-4C4B-ADF8-02886CECD41F}.Release|Any CPU.Build.0 = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(SolutionProperties) = preSolution 19 | HideSolutionNode = FALSE 20 | EndGlobalSection 21 | EndGlobal 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Chedy Missaoui 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Models/PersonViewModel.cs: -------------------------------------------------------------------------------- 1 | using DynamicFormTagHelper.TagHelpers; 2 | using Microsoft.AspNetCore.Mvc.Rendering; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | namespace DynamicFormTagHelper.Models 8 | { 9 | public enum Sex 10 | { 11 | MALE, 12 | FEMALE, 13 | OTHER 14 | } 15 | 16 | public class PersonViewModel 17 | { 18 | [Required] 19 | [Display(Name = "Person's identification number")] 20 | public int? Id { get; set; } 21 | 22 | [Required] 23 | [Display(Name = "Person's full name")] 24 | [AutoComplete(SuggestionsProperty=nameof(SuggestedNames))] 25 | public string Name { get; set; } 26 | 27 | public List SuggestedNames => new List() { "John", "Johnson", "Chirmoi", "Bay Sargo", "Mahmoud"}; 28 | 29 | [Required] 30 | [Display(Name = "Person's Gender")] 31 | [ItemsSource(ItemsEnum = typeof(Sex))] 32 | public Sex Gender { get; set; } 33 | 34 | [DataType(DataType.Date)] 35 | [Display(Name = "Birth date")] 36 | public DateTime? BirthDate { get; set; } 37 | 38 | [Display(Name = "BasketBall Player?")] 39 | public bool IsBasketBallPlayer { get; set; } = false; 40 | 41 | [Required] 42 | [Display(Name = "Item")] 43 | [ItemsSource(ItemsProperty = nameof(Items))] 44 | public string SelectedItem { get; set; } 45 | 46 | [Required] 47 | [Display(Name = "Multiple Items")] 48 | [ItemsSource(ItemsProperty = nameof(Items))] 49 | public List SelectedItems { get; set; } 50 | 51 | [Required] 52 | [Display(Name = "Single Item")] 53 | [ItemsSource(ItemsProperty = nameof(Items), ChoicesType = ChoicesTypes.RADIO)] 54 | public string SingleItem { get; set; } 55 | 56 | 57 | 58 | [Display(Name = "Person's Dog")] 59 | public DogViewModel OwnedDog { get; set; } = new DogViewModel(); 60 | 61 | public string IsBasketBallPlayerVM => IsBasketBallPlayer ? "Yes" : "No"; 62 | 63 | public List Items => new List() 64 | { 65 | new SelectListItem {Value = "0", Text="Glove", Selected = true}, 66 | new SelectListItem {Value = "1", Text="Umbrella"}, 67 | new SelectListItem {Value = "2", Text="Laptop"}, 68 | new SelectListItem {Value = "3", Text="Gamepad"}, 69 | }; 70 | } 71 | 72 | public class DogViewModel 73 | { 74 | [Display(Name="Dog's Id")] 75 | public int Id { get; set; } 76 | 77 | [Required] 78 | [Display(Name = "Dog's Name")] 79 | [AutoComplete(SuggestionsEndpoint = "/Home/DogNames")] 80 | public string Name { get; set; } 81 | 82 | [DataType(DataType.Date)] 83 | [Display(Name = "Birth date")] 84 | public DateTime BirthDate { get; set; } 85 | 86 | public Leash DogLeash { get; set; } = new Leash(); 87 | } 88 | 89 | public class Leash 90 | { 91 | public string Name { get; set; } 92 | 93 | [Required] 94 | [Display(Name="Type of the leash")] 95 | [ItemsSource(ItemsProperty = nameof(KindsOfLeashes))] 96 | public int LeashKind { get; set; } 97 | 98 | public List KindsOfLeashes => new List 99 | { 100 | new SelectListItem(){Value = "0", Text = "High end"}, 101 | new SelectListItem(){Value = "1", Text = "Mid tier"}, 102 | new SelectListItem(){Value = "2", Text = "Low end"}, 103 | }; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Hosting; 7 | 8 | namespace WebApplication 9 | { 10 | public class Program 11 | { 12 | public static void Main(string[] args) 13 | { 14 | var host = new WebHostBuilder() 15 | .UseKestrel() 16 | .UseContentRoot(Directory.GetCurrentDirectory()) 17 | .UseIISIntegration() 18 | .UseStartup() 19 | .Build(); 20 | 21 | host.Run(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:58419/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASP.NET Core Dynamic Form Tag Helper 2 | 3 | Full code for the following posts on the TechDominator Blog (ordered by least recent): 4 | - [First Dynamic Form Tag Helper Attempt](http://blog.techdominator.com/article/first-dynamic-form-tag-helper-attempt.html) 5 | - [Supporting Complex Type Properties in The Dynamic Form Tag Helper](http://blog.techdominator.com/article/supporting-complex-type-properties-in-the-dynamic-form-tag-helper.html) 6 | - [Customizing Dynamic Form Tag Helper Generation](http://blog.techdominator.com/article/customizing-dynamic-form-tag-helper-generation.html) 7 | - [Handling Multiple Choices Fields In The Dynamic Form Tag Helper](http://blog.techdominator.com/article/handling-multiple-choices-fields-in-the-dynamic-form-tag-helper.html) 8 | - [Auto-complete for the Dynamic Form Tag Helper](http://blog.techdominator.com/article/auto-complete-for-the-dynamic-form-tag-helper-text-fields.html) 9 | # Guidelines 10 | 11 | This is a Visual Studio 2017 Asp.net Core (.net core runtime) project. Installation instructions can be found [here](https://docs.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/start-mvc). 12 | 13 | Code as described in the [First Dynamic Form Tag Helper Attempt](http://blog.techdominator.com/article/first-dynamic-form-tag-helper-attempt.html) post is available on the **'first_attempt'** tag. 14 | 15 | Changes introduced in the [Supporting Complex Type Properties in The Dynamic Form Tag Helper](http://blog.techdominator.com/article/supporting-complex-type-properties-in-the-dynamic-form-tag-helper.html) post are available in the **'complex_properties'** tag. 16 | 17 | Changes introduced in the [Customizing Dynamic Form Tag Helper Generation](http://blog.techdominator.com/article/customizing-dynamic-form-tag-helper-generation.html) post are available in the **'tweak_nested_tag_helper'** tag. 18 | 19 | Changes introduced in the [Handling Multiple Choices Fields In The Dynamic Form Tag Helper](http://blog.techdominator.com/article/handling-multiple-choices-fields-in-the-dynamic-form-tag-helper.html) post are available in the **multiple_choices** tag. 20 | 21 | Changes introduced in the [Auto-complete for the Dynamic Form Tag Helper](http://blog.techdominator.com/article/auto-complete-for-the-dynamic-form-tag-helper-text-fields.html) post are available in the most recent commit. 22 | 23 | The Tag helper used in the tutorial is defined under the `DynamicFormTagHelper/TagHelpers` folder and is used in the `Index` view. 24 | 25 | The `FormGroupBuilder` class is defined under the `DynamicFormTagHelper/TagHelpers` folder. 26 | 27 | Custom attributes are defined under the `DynamicFormTagHelper/TagHelpers` folder. 28 | -------------------------------------------------------------------------------- /Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Logging; 11 | 12 | 13 | namespace WebApplication 14 | { 15 | public class Startup 16 | { 17 | public Startup(IHostingEnvironment env) 18 | { 19 | var builder = new ConfigurationBuilder() 20 | .SetBasePath(env.ContentRootPath) 21 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) 22 | .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); 23 | builder.AddEnvironmentVariables(); 24 | Configuration = builder.Build(); 25 | } 26 | 27 | public IConfigurationRoot Configuration { get; } 28 | 29 | // This method gets called by the runtime. Use this method to add services to the container. 30 | public void ConfigureServices(IServiceCollection services) 31 | { 32 | services.AddMvc(); 33 | } 34 | 35 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 36 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 37 | { 38 | loggerFactory.AddConsole(Configuration.GetSection("Logging")); 39 | loggerFactory.AddDebug(); 40 | 41 | if (env.IsDevelopment()) 42 | { 43 | app.UseDeveloperExceptionPage(); 44 | app.UseBrowserLink(); 45 | } 46 | else 47 | { 48 | app.UseExceptionHandler("/Home/Error"); 49 | } 50 | 51 | app.UseStaticFiles(); 52 | 53 | 54 | // Add external authentication middleware below. To configure them please see https://go.microsoft.com/fwlink/?LinkID=532715 55 | 56 | app.UseMvc(routes => 57 | { 58 | routes.MapRoute( 59 | name: "default", 60 | template: "{controller=Home}/{action=Index}/{id?}"); 61 | }); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /TagHelpers/AutoCompleteAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.AspNetCore.Mvc.ViewFeatures; 5 | using Newtonsoft.Json; 6 | 7 | namespace DynamicFormTagHelper.TagHelpers 8 | { 9 | [AttributeUsage(AttributeTargets.Property, Inherited = false)] 10 | public class AutoCompleteAttribute : Attribute 11 | { 12 | public string SuggestionsProperty { get; set; } 13 | 14 | public string SuggestionsEndpoint { get; set; } 15 | 16 | public string GetSuggestionsAsJson(ModelExplorer modelExplorer) 17 | { 18 | var properties = modelExplorer.Properties 19 | .Where(p => p.Metadata.PropertyName.Equals(SuggestionsProperty)); 20 | if (properties.Count() == 1) 21 | { 22 | return JsonConvert.SerializeObject(properties.First().Model as IEnumerable); 23 | } 24 | return string.Empty; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /TagHelpers/DynamicFormTagHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Mvc.Rendering; 4 | using Microsoft.AspNetCore.Mvc.ViewFeatures; 5 | using Microsoft.AspNetCore.Razor.TagHelpers; 6 | using System.Text.Encodings.Web; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Text.RegularExpressions; 10 | 11 | namespace DynamicFormTagHelper.TagHelpers 12 | { 13 | public class PropertyTweakingConfiguration 14 | { 15 | public ModelExpression Property { get; set; } 16 | public string InputTemplatePath { get; set; } 17 | public string LabelClasses { get; set; } 18 | public string InputClasses { get; set; } 19 | public string ValidationClasses { get; set; } 20 | } 21 | 22 | public class TweakingConfiguration 23 | { 24 | private List _propertyEntries = new List(); 25 | 26 | public void Add(PropertyTweakingConfiguration propertyConfig) 27 | { 28 | _propertyEntries.Add(propertyConfig); 29 | } 30 | 31 | public PropertyTweakingConfiguration GetByPropertyFullName(string fullName) 32 | { 33 | var found = _propertyEntries.Where(e => e.Property.Name.Equals(fullName)); 34 | if (found.Count() > 0) 35 | { 36 | return found.First(); 37 | } 38 | else 39 | { 40 | return null; 41 | } 42 | } 43 | } 44 | 45 | [RestrictChildren("tweak")] 46 | public class DynamicFormTagHelper : TagHelper 47 | { 48 | [HtmlAttributeName("asp-action")] 49 | public string TargetAction { get; set; } 50 | 51 | [HtmlAttributeName("asp-model")] 52 | public ModelExpression Model { get; set; } 53 | 54 | [HtmlAttributeName("asp-submit-button-text")] 55 | public string SubmitButtonText { get; set; } 56 | 57 | [HtmlAttributeName("asp-submit-button-classes")] 58 | public string SubmitButtonClasses { get; set; } 59 | 60 | [ViewContext] 61 | public ViewContext ViewContext { get; set; } 62 | 63 | private HtmlEncoder _encoder { get; set; } 64 | 65 | private IHtmlGenerator _htmlGenerator; 66 | 67 | private IHtmlHelper _htmlHelper; 68 | 69 | public DynamicFormTagHelper(IHtmlGenerator htmlGenerator, HtmlEncoder encoder, IHtmlHelper htmlHelper) 70 | { 71 | _htmlGenerator = htmlGenerator; 72 | _encoder = encoder; 73 | _htmlHelper = htmlHelper; 74 | } 75 | 76 | public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) 77 | { 78 | FormGroupBuilder formGroupBuilder = new FormGroupBuilder(_htmlGenerator, ViewContext, 79 | _encoder, _htmlHelper); 80 | 81 | TweakingConfiguration tweakingConfig = new TweakingConfiguration(); 82 | context.Items.Add("TweakingConfig", tweakingConfig); 83 | await output.GetChildContentAsync(); 84 | 85 | StringBuilder builder = new StringBuilder(); 86 | foreach (ModelExplorer prop in Model.ModelExplorer.Properties) 87 | { 88 | builder.Append(await formGroupBuilder.GetFormGroup(prop, tweakingConfig)); 89 | } 90 | 91 | if (string.IsNullOrEmpty(SubmitButtonText)) SubmitButtonText = "Create"; 92 | builder.Append($@"
93 | 94 |
"); 95 | 96 | output.TagName = "form"; 97 | output.Attributes.Add(new TagHelperAttribute("method", "post")); 98 | output.Attributes.Add(new TagHelperAttribute("action", TargetAction)); 99 | output.Attributes.Add(new TagHelperAttribute("autocomplete", "off")); 100 | output.Content.SetHtmlContent(builder.ToString()); 101 | } 102 | private string mergeClasses(string userProvidedClasses) 103 | { 104 | userProvidedClasses = Regex.Replace(userProvidedClasses, "btn\b", ""); 105 | userProvidedClasses = userProvidedClasses.Replace("btn-primary", ""); 106 | return $"btn btn-primary {userProvidedClasses.Trim()}"; 107 | } 108 | } 109 | 110 | [HtmlTargetElement(ParentTag = "dynamic-form")] 111 | public class TweakTagHelper : TagHelper 112 | { 113 | [HtmlAttributeName("asp-property")] 114 | public ModelExpression TweakedProperty { get; set; } 115 | 116 | [HtmlAttributeName("asp-input-path")] 117 | public string InputTemplatePath { get; set; } 118 | 119 | [HtmlAttributeName("asp-label-classes")] 120 | public string LabelClasses { get; set; } 121 | 122 | [HtmlAttributeName("asp-input-classes")] 123 | public string InputClasses { get; set; } 124 | 125 | [HtmlAttributeName("asp-validation-classes")] 126 | public string ValidationClasses { get; set; } 127 | 128 | public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) 129 | { 130 | TweakingConfiguration overridingConfig = context.Items["TweakingConfig"] as TweakingConfiguration; 131 | overridingConfig.Add(new PropertyTweakingConfiguration 132 | { 133 | Property = TweakedProperty, 134 | InputTemplatePath = InputTemplatePath, 135 | LabelClasses = LabelClasses, 136 | InputClasses = InputClasses, 137 | ValidationClasses = ValidationClasses 138 | }); 139 | output.SuppressOutput(); 140 | return Task.CompletedTask; 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /TagHelpers/FormGroupBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Threading.Tasks; 4 | using System.Collections.Generic; 5 | using Microsoft.AspNetCore.Mvc.Rendering; 6 | using Microsoft.AspNetCore.Mvc.TagHelpers; 7 | using Microsoft.AspNetCore.Mvc.ViewFeatures; 8 | using Microsoft.AspNetCore.Razor.TagHelpers; 9 | using System.IO; 10 | using System.Text.Encodings.Web; 11 | using System.Linq; 12 | using Microsoft.AspNetCore.Html; 13 | using System.Reflection; 14 | 15 | namespace DynamicFormTagHelper.TagHelpers 16 | { 17 | public class FormGroupBuilder 18 | { 19 | private readonly IHtmlGenerator _htmlGenerator; 20 | private readonly ViewContext _viewContext; 21 | private readonly HtmlEncoder _htmlEncoder; 22 | private readonly IHtmlHelper _htmlHelper; 23 | 24 | public FormGroupBuilder(IHtmlGenerator htmlGenerator, 25 | ViewContext viewContext, 26 | HtmlEncoder htmlEncoder, 27 | IHtmlHelper htmlHelper) 28 | { 29 | _htmlGenerator = htmlGenerator; 30 | _viewContext = viewContext; 31 | _htmlEncoder = htmlEncoder; 32 | _htmlHelper = htmlHelper; 33 | (_htmlHelper as IViewContextAware).Contextualize(this._viewContext); 34 | } 35 | 36 | public Task GetFormGroup(ModelExplorer property, TweakingConfiguration tweakingConfig) 37 | { 38 | if (!property.IsReadOnly()) 39 | { 40 | ItemsSourceAttribute itemSource; 41 | if (property.HasValueFromSelectList(out itemSource)) 42 | { 43 | return _getFormGroupForSelectList(property, itemSource.GetItems(property.Container), 44 | itemSource.ChoicesType, tweakingConfig); 45 | } 46 | else if (property.ModelType.IsSimpleType()) 47 | { 48 | return _getFormGroupForSimpleProperty(property, tweakingConfig); 49 | } 50 | else 51 | { 52 | return _getFormGroupsForComplexProperty(property, tweakingConfig); 53 | } 54 | } 55 | else 56 | { 57 | return Task.Run(() => String.Empty); 58 | } 59 | } 60 | 61 | private async Task _getFormGroupForSelectList(ModelExplorer property, IEnumerable items, 62 | ChoicesTypes choicesType, TweakingConfiguration tweakingConfig) 63 | { 64 | string label = await buildLabelHtml(property, tweakingConfig); 65 | 66 | string select = ""; 67 | if (choicesType == ChoicesTypes.RADIO) 68 | { 69 | select = await buildRadioInputsHtml(property, items, tweakingConfig); 70 | } 71 | else 72 | { 73 | select = await buildSelectHtml(property, items, tweakingConfig); 74 | } 75 | 76 | string validation = await buildValidationMessageHtml(property, tweakingConfig); 77 | return $@"
78 | {label} 79 | {select} 80 | {validation} 81 |
"; 82 | } 83 | 84 | private async Task _getFormGroupForSimpleProperty(ModelExplorer property, 85 | TweakingConfiguration tweakingConfig) 86 | { 87 | string label = await buildLabelHtml(property, tweakingConfig); 88 | 89 | string input = await buildInputHtml(property, tweakingConfig); 90 | 91 | string validation = await buildValidationMessageHtml(property, tweakingConfig); 92 | return $@"
93 | {label} 94 | {input} 95 | {validation} 96 |
"; 97 | } 98 | 99 | private async Task _getFormGroupsForComplexProperty(ModelExplorer property, 100 | TweakingConfiguration config) 101 | { 102 | StringBuilder builder = new StringBuilder(); 103 | 104 | string label = await buildLabelHtml(property, config); 105 | foreach (var prop in property.Properties) 106 | { 107 | builder.Append(await GetFormGroup(prop, config)); 108 | } 109 | 110 | return $@"
111 | {label} 112 |
113 | {builder.ToString()} 114 |
115 |
"; 116 | } 117 | private async Task buildLabelHtml(ModelExplorer property, TweakingConfiguration tweakingConfig) 118 | { 119 | TagHelper label = new LabelTagHelper(_htmlGenerator) 120 | { 121 | For = new ModelExpression(property.GetFullName(), property), 122 | ViewContext = _viewContext 123 | }; 124 | 125 | PropertyTweakingConfiguration propertyConfig = tweakingConfig 126 | .GetByPropertyFullName(property.GetFullName()); 127 | return await GetGeneratedContentFromTagHelper( 128 | "label", 129 | TagMode.StartTagAndEndTag, label, 130 | new TagHelperAttributeList() { 131 | new TagHelperAttribute("class", propertyConfig?.LabelClasses) 132 | }); 133 | } 134 | 135 | private async Task buildRadioInputsHtml(ModelExplorer property, 136 | IEnumerable items, 137 | TweakingConfiguration tweakingConfig) 138 | { 139 | StringBuilder inputs = new StringBuilder(); 140 | foreach (var item in items) 141 | { 142 | inputs.Append($"
{await buildInputHtml(property, tweakingConfig, "radio", item.Value, item.Selected)} {item.Text}"); 143 | } 144 | return inputs.ToString(); 145 | } 146 | 147 | private async Task buildSelectHtml(ModelExplorer property, IEnumerable items, TweakingConfiguration tweakingConfig) 148 | { 149 | TagHelper select = new SelectTagHelper(_htmlGenerator) 150 | { 151 | For = new ModelExpression(property.GetFullName(), property), 152 | ViewContext = _viewContext, 153 | Items = items 154 | }; 155 | 156 | return await GetGeneratedContentFromTagHelper("select", 157 | TagMode.StartTagAndEndTag, 158 | select, 159 | new TagHelperAttributeList { new TagHelperAttribute("class", "form-control dropdown") }); 160 | } 161 | 162 | 163 | 164 | private async Task buildInputHtml(ModelExplorer property, TweakingConfiguration tweakingConfig, 165 | string inputType="", string inputValue="", bool radioChecked = false) 166 | { 167 | PropertyTweakingConfiguration propertyConfig = tweakingConfig.GetByPropertyFullName(property.GetFullName()); 168 | if (propertyConfig == null || string.IsNullOrEmpty(propertyConfig.InputTemplatePath)) 169 | { 170 | InputTagHelper input = new InputTagHelper(_htmlGenerator) 171 | { 172 | For = new ModelExpression(property.GetFullName(), property), 173 | ViewContext = _viewContext 174 | }; 175 | var attrs = new TagHelperAttributeList { new TagHelperAttribute("class", $"form-control {propertyConfig?.InputClasses}") }; 176 | 177 | if (!string.IsNullOrEmpty(inputType)) 178 | { 179 | input.InputTypeName = inputType; 180 | input.Value = inputValue; 181 | // Setting the Type attributes requires providing an initialized 182 | // AttributeList with the type attribute 183 | attrs = new TagHelperAttributeList() 184 | { 185 | new TagHelperAttribute("class", $"{propertyConfig?.InputClasses}"), 186 | new TagHelperAttribute("type", inputType), 187 | new TagHelperAttribute("value", inputValue) 188 | }; 189 | if (radioChecked) 190 | { 191 | attrs.Add(new TagHelperAttribute("checked", "checked")); 192 | } 193 | } 194 | 195 | AutoCompleteAttribute autoComplete; 196 | if (property.HasAutoComplete(out autoComplete)) 197 | { 198 | attrs.AddClass("autocomplete"); 199 | attrs.Add(getAutoCompleteDataAttribute(autoComplete, property.Container)); 200 | } 201 | 202 | return await GetGeneratedContentFromTagHelper("input", 203 | TagMode.SelfClosing, 204 | input, 205 | attributes: attrs 206 | ); 207 | } 208 | else 209 | { 210 | return renderInputTemplate(property, propertyConfig.InputTemplatePath); 211 | } 212 | } 213 | 214 | private TagHelperAttribute getAutoCompleteDataAttribute(AutoCompleteAttribute autoComplete, ModelExplorer parentModel) 215 | { 216 | if (!string.IsNullOrEmpty(autoComplete.SuggestionsProperty)) 217 | { 218 | string suggestions = autoComplete.GetSuggestionsAsJson(parentModel); 219 | return new TagHelperAttribute("data-source-local", suggestions); 220 | } 221 | else if (!string.IsNullOrEmpty(autoComplete.SuggestionsEndpoint)) 222 | { 223 | return new TagHelperAttribute("data-source-ajax", autoComplete.SuggestionsEndpoint); 224 | } 225 | return null; 226 | } 227 | private async Task buildValidationMessageHtml(ModelExplorer property, TweakingConfiguration tweakingConfig) 228 | { 229 | PropertyTweakingConfiguration propertyConfig = tweakingConfig.GetByPropertyFullName(property.GetFullName()); 230 | 231 | TagHelper validationMessage = new ValidationMessageTagHelper(_htmlGenerator) 232 | { 233 | For = new ModelExpression(property.GetFullName(), property), 234 | ViewContext = _viewContext 235 | }; 236 | return await GetGeneratedContentFromTagHelper("span", 237 | TagMode.StartTagAndEndTag, 238 | validationMessage, 239 | new TagHelperAttributeList() { new TagHelperAttribute("class", propertyConfig?.ValidationClasses) }); 240 | } 241 | 242 | private async Task GetGeneratedContentFromTagHelper(string tagName, TagMode tagMode, 243 | ITagHelper tagHelper, TagHelperAttributeList attributes = null) 244 | { 245 | if (attributes == null) 246 | { 247 | attributes = new TagHelperAttributeList(); 248 | } 249 | 250 | TagHelperOutput output = new TagHelperOutput(tagName, attributes, (arg1, arg2) => 251 | { 252 | return Task.Run(() => new DefaultTagHelperContent()); 253 | }) 254 | { 255 | TagMode = tagMode 256 | }; 257 | TagHelperContext context = new TagHelperContext(attributes, new Dictionary(), Guid.NewGuid().ToString()); 258 | 259 | tagHelper.Init(context); 260 | await tagHelper.ProcessAsync(context, output); 261 | 262 | return output.RenderTag(_htmlEncoder); 263 | } 264 | 265 | private string renderInputTemplate(ModelExplorer property, string path) 266 | { 267 | return _htmlHelper.Editor(property.Metadata.PropertyName, path).RenderTag(_htmlEncoder); 268 | } 269 | } 270 | 271 | #region Utility_Extension_Methods 272 | public static class Extensions 273 | { 274 | public static string GetFullName(this ModelExplorer property) 275 | { 276 | List nameComponents = new List { property.Metadata.PropertyName }; 277 | 278 | while (!string.IsNullOrEmpty(property.Container.Metadata.PropertyName)) 279 | { 280 | nameComponents.Add(property.Container.Metadata.PropertyName); 281 | property = property.Container; 282 | } 283 | 284 | nameComponents.Reverse(); 285 | return string.Join(".", nameComponents); 286 | } 287 | public static bool IsSimpleType(this Type propertyType) 288 | { 289 | Type[] simpleTypes = new Type[] 290 | { 291 | typeof(string), typeof(bool), typeof(byte), typeof(char), typeof(DateTime), typeof(DateTimeOffset), 292 | typeof(decimal), typeof(double), typeof(Guid), typeof(short), typeof(int), typeof(long), typeof(float), 293 | typeof(TimeSpan), typeof(ushort), typeof(uint),typeof(ulong) 294 | }; 295 | if (propertyType.IsConstructedGenericType && propertyType.Name.Equals("Nullable`1")) 296 | { 297 | return IsSimpleType(propertyType.GenericTypeArguments.First()); 298 | } 299 | else 300 | { 301 | return (!propertyType.IsArray && !propertyType.IsPointer && simpleTypes.Contains(propertyType)); 302 | } 303 | } 304 | public static string RenderTag(this IHtmlContent output, HtmlEncoder encoder) 305 | { 306 | using (var writer = new StringWriter()) 307 | { 308 | output.WriteTo(writer, encoder); 309 | return writer.ToString(); 310 | } 311 | } 312 | public static bool HasValueFromSelectList(this ModelExplorer property, out ItemsSourceAttribute itemSource) 313 | { 314 | return property.HasAttribute(out itemSource); 315 | } 316 | 317 | public static bool HasAutoComplete(this ModelExplorer property, out AutoCompleteAttribute autoComplete) 318 | { 319 | return property.HasAttribute(out autoComplete); 320 | } 321 | 322 | public static bool HasAttribute(this ModelExplorer property, out T attribute) where T : Attribute 323 | { 324 | attribute = property.Container.ModelType.GetTypeInfo() 325 | .GetProperty(property.Metadata.PropertyName) 326 | .GetCustomAttribute(); 327 | 328 | return (attribute != null); 329 | } 330 | public static bool IsReadOnly(this ModelExplorer property) 331 | { 332 | return property.Metadata.PropertySetter == null; 333 | } 334 | 335 | public static void AddClass(this TagHelperAttributeList attrList, string cssClass) 336 | { 337 | var attrs = attrList.Where(a => a.Name.Equals("class")); 338 | if (attrs.Count() == 1) 339 | { 340 | TagHelperAttribute classAttr = attrs.First(); 341 | attrList.Remove(classAttr); 342 | attrList.Add(new TagHelperAttribute("class", $"{classAttr.Value} autocomplete")); 343 | } 344 | } 345 | } 346 | #endregion 347 | } 348 | -------------------------------------------------------------------------------- /TagHelpers/ItemsSourceAttribute.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Rendering; 2 | using Microsoft.AspNetCore.Mvc.ViewFeatures; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | namespace DynamicFormTagHelper.TagHelpers 9 | { 10 | public enum ChoicesTypes 11 | { 12 | DEFAULT, 13 | RADIO 14 | } 15 | 16 | [AttributeUsage(AttributeTargets.Property, Inherited = false)] 17 | public class ItemsSourceAttribute : Attribute 18 | { 19 | public string ItemsProperty { get; set; } 20 | public Type ItemsEnum { get; set; } 21 | public ChoicesTypes ChoicesType { get; set; } = ChoicesTypes.DEFAULT; 22 | 23 | public IEnumerable GetItems(ModelExplorer explorer) 24 | { 25 | if ((ItemsEnum != null) && (ItemsEnum.GetTypeInfo().IsEnum)) 26 | { 27 | var items = new List(); 28 | MemberInfo[] enumItems = ItemsEnum.GetTypeInfo().GetMembers(BindingFlags.Public | BindingFlags.Static); 29 | for (int i = 0; i < enumItems.Length; i++) 30 | { 31 | items.Add(new SelectListItem() { Value = i.ToString(), Text = enumItems[i].Name }); 32 | } 33 | 34 | return items; 35 | } 36 | else 37 | { 38 | var properties = explorer.Properties.Where(p => p.Metadata.PropertyName.Equals(ItemsProperty)); 39 | if (properties.Count() == 1) 40 | { 41 | return properties.First().Model as IEnumerable; 42 | } 43 | return new List(); 44 | } 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Views/Home/About.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "About"; 3 | } 4 |

@ViewData["Title"].

5 |

@ViewData["Message"]

6 | 7 |

Use this area to provide additional information.

8 | -------------------------------------------------------------------------------- /Views/Home/Contact.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Contact"; 3 | } 4 |

@ViewData["Title"].

5 |

@ViewData["Message"]

6 | 7 |
8 | One Microsoft Way
9 | Redmond, WA 98052-6399
10 | P: 11 | 425.555.0100 12 |
13 | 14 |
15 | Support: Support@example.com
16 | Marketing: Marketing@example.com 17 |
18 | -------------------------------------------------------------------------------- /Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model DynamicFormTagHelper.Models.PersonViewModel 2 | @{ 3 | ViewData["Title"] = "Home Page"; 4 | } 5 |
6 |
7 | 10 | 11 | 13 | 14 | 15 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | @*Includes in the Scripts section under Shared/_Layout.cshtml the Jquery validation scripts*@ 29 | @section Scripts { 30 | @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} 31 | } 32 | -------------------------------------------------------------------------------- /Views/Home/Show.cshtml: -------------------------------------------------------------------------------- 1 | @model DynamicFormTagHelper.Models.PersonViewModel 2 | @{ 3 | ViewData["Title"] = "Show"; 4 | } 5 |
6 |

Id: @Model.Id

7 |

Name: @Model.Name

8 |

Gender: @Model.Gender

9 |

Birth: @Model.BirthDate

10 |

BasketBall?: @Model.IsBasketBallPlayerVM

11 |

Item from drop down @Model.SelectedItem

12 |

Item from radio group @Model.SingleItem

13 |

Items from multi select @(String.Join(", ", Model.SelectedItems))

14 |

Dog: @(Model.OwnedDog.Id) > @(Model.OwnedDog.Name) > @(Model.OwnedDog.DogLeash.Name) > @(Model.OwnedDog.DogLeash.LeashKind)

15 |
-------------------------------------------------------------------------------- /Views/Shared/EditorTemplates/DateTime.cshtml: -------------------------------------------------------------------------------- 1 | @model DateTime? 2 | 3 |
4 | 5 | 6 | 7 | 8 |
-------------------------------------------------------------------------------- /Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Error"; 3 | } 4 | 5 |

Error.

6 |

An error occurred while processing your request.

7 | 8 |

Development Mode

9 |

10 | Swapping to Development environment will display more detailed information about the error that occurred. 11 |

12 |

13 | Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. 14 |

15 | -------------------------------------------------------------------------------- /Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | @ViewData["Title"] - WebApplication 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 42 |
43 | @RenderBody() 44 |
45 |
46 |

© 2016 - WebApplication

47 |
48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 63 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | @RenderSection("scripts", required: false) 74 | 75 | 76 | -------------------------------------------------------------------------------- /Views/Shared/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using WebApplication 2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 3 | @addTagHelper *, DynamicFormTagHelper 4 | -------------------------------------------------------------------------------- /Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Data Source=WebApplication.db" 4 | }, 5 | "Logging": { 6 | "IncludeScopes": false, 7 | "LogLevel": { 8 | "Default": "Debug", 9 | "System": "Information", 10 | "Microsoft": "Information" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webapplication", 3 | "private": true, 4 | "dependencies": { 5 | "bootstrap": "3.3.6", 6 | "jquery": "2.2.3", 7 | "jquery-validation": "1.15.0", 8 | "jquery-validation-unobtrusive": "3.2.6", 9 | "eonasdan-bootstrap-datetimepicker": "4.17.47", 10 | "typeahead.js": "0.11.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /// 2 | "use strict"; 3 | 4 | var gulp = require("gulp"), 5 | rimraf = require("rimraf"), 6 | concat = require("gulp-concat"), 7 | cssmin = require("gulp-cssmin"), 8 | uglify = require("gulp-uglify"); 9 | 10 | var webroot = "./wwwroot/"; 11 | 12 | var paths = { 13 | js: webroot + "js/**/*.js", 14 | minJs: webroot + "js/**/*.min.js", 15 | css: webroot + "css/**/*.css", 16 | minCss: webroot + "css/**/*.min.css", 17 | concatJsDest: webroot + "js/site.min.js", 18 | concatCssDest: webroot + "css/site.min.css" 19 | }; 20 | 21 | gulp.task("clean:js", function (cb) { 22 | rimraf(paths.concatJsDest, cb); 23 | }); 24 | 25 | gulp.task("clean:css", function (cb) { 26 | rimraf(paths.concatCssDest, cb); 27 | }); 28 | 29 | gulp.task("clean", ["clean:js", "clean:css"]); 30 | 31 | gulp.task("min:js", function () { 32 | return gulp.src([paths.js, "!" + paths.minJs], { base: "." }) 33 | .pipe(concat(paths.concatJsDest)) 34 | .pipe(uglify()) 35 | .pipe(gulp.dest(".")); 36 | }); 37 | 38 | gulp.task("min:css", function () { 39 | return gulp.src([paths.css, "!" + paths.minCss]) 40 | .pipe(concat(paths.concatCssDest)) 41 | .pipe(cssmin()) 42 | .pipe(gulp.dest(".")); 43 | }); 44 | 45 | gulp.task("min", ["min:js", "min:css"]); 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webapplication", 3 | "version": "0.0.0", 4 | "private": true, 5 | "devDependencies": { 6 | "gulp": "3.9.1", 7 | "gulp-concat": "2.6.0", 8 | "gulp-cssmin": "0.1.7", 9 | "gulp-uglify": "1.5.3", 10 | "rimraf": "2.5.2" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | padding-bottom: 20px; 4 | } 5 | 6 | /* Wrapping element */ 7 | /* Set some basic padding to keep content from hitting the edges */ 8 | .body-content { 9 | padding-left: 15px; 10 | padding-right: 15px; 11 | } 12 | 13 | /* Set widths on the form inputs since otherwise they're 100% wide */ 14 | input, 15 | select, 16 | textarea { 17 | max-width: 280px; 18 | } 19 | 20 | /* Carousel */ 21 | .carousel-caption p { 22 | font-size: 20px; 23 | line-height: 1.4; 24 | } 25 | 26 | /* buttons and links extension to use brackets: [ click me ] */ 27 | .btn-bracketed::before { 28 | display: inline-block; 29 | content: "["; 30 | padding-right: 0.5em; 31 | } 32 | 33 | .btn-bracketed::after { 34 | display: inline-block; 35 | content: "]"; 36 | padding-left: 0.5em; 37 | } 38 | 39 | /* Hide/rearrange for smaller screens */ 40 | @media screen and (max-width: 767px) { 41 | /* Hide captions */ 42 | .carousel-caption { 43 | display: none 44 | } 45 | } 46 | 47 | span.field-validation-error span { 48 | display: inline-block; 49 | color: red; 50 | font-weight: bold; 51 | border: 3px red solid; 52 | background-color: black; 53 | } 54 | 55 | .sub-form-group { 56 | padding-left: 1em; 57 | } 58 | 59 | .custom-input-class { 60 | color: greenyellow; 61 | } 62 | 63 | .custom-label-class { 64 | color: blueviolet; 65 | } 66 | 67 | /* Style for the autocomplete */ 68 | 69 | .twitter-typeahead { 70 | display: inline-block; 71 | width: 100%; 72 | } 73 | 74 | .tt-menu { 75 | border-color: black; 76 | border-width: 0.5px; 77 | border-style: solid; 78 | border-radius: 4px; 79 | background-color: white; 80 | padding-top: 0.1em; 81 | padding-bottom: 0.1em; 82 | width: 100%; 83 | } 84 | 85 | .tt-menu .tt-suggestion { 86 | padding-left: 0.1em; 87 | padding-left: 0.25em; 88 | } 89 | 90 | .tt-menu .tt-suggestion:hover { 91 | cursor: pointer; 92 | } 93 | 94 | .tt-cursor { 95 | background-color: lightgrey; 96 | } 97 | 98 | -------------------------------------------------------------------------------- /wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MissaouiChedy/DynamicFormTagHelper/8e1d43bbfdc56718ec0e106767499cabd5aba1f8/wwwroot/favicon.ico -------------------------------------------------------------------------------- /wwwroot/images/banner1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wwwroot/images/banner2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wwwroot/images/banner3.svg: -------------------------------------------------------------------------------- 1 | banner3b -------------------------------------------------------------------------------- /wwwroot/images/banner4.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | function initializeAutoComplete() { 2 | // initialize auto complete fields 3 | $('input.autocomplete').each(function (index, elem) { 4 | var suggestionEngine = null; 5 | 6 | if ($(elem).data('source-local')) { 7 | // initialize from local source 8 | suggestionEngine = new Bloodhound({ 9 | datumTokenizer: Bloodhound.tokenizers.whitespace, 10 | queryTokenizer: Bloodhound.tokenizers.whitespace, 11 | local: $(elem).data('source-local') 12 | }); 13 | 14 | } 15 | else if ($(elem).data('source-ajax')) { 16 | // initialize from remote source 17 | suggestionEngine = new Bloodhound({ 18 | datumTokenizer: function (datum) { 19 | return Bloodhound.tokenizers.whitespace(datum); 20 | }, 21 | queryTokenizer: Bloodhound.tokenizers.whitespace, 22 | remote: { 23 | url: $(elem).data('source-ajax') + '?typed=%QUERY', 24 | wildcard: '%QUERY' 25 | } 26 | }); 27 | } 28 | 29 | if (suggestionEngine !== null) { 30 | suggestionEngine.initialize(); 31 | $(elem).typeahead({ 32 | hint: true, 33 | highlight: true, 34 | minLength: 1 35 | }, { 36 | source: suggestionEngine.ttAdapter(), 37 | autoSelect: true 38 | }); 39 | } 40 | }); 41 | } 42 | 43 | $(document).ready(function () { 44 | $('.datetimepicker').each(function (index, elem) { 45 | $(elem).datetimepicker({ 46 | format: "YYYY-MM-DD[T]HH:mm:ss.SSS[Z]", 47 | showClear: true, 48 | showTodayButton: true 49 | }); 50 | }); 51 | 52 | initializeAutoComplete(); 53 | }); 54 | --------------------------------------------------------------------------------