Created
April 18, 2021 02:44
-
-
Save daiplusplus/844ae242025e0f0b750503629d46087e to your computer and use it in GitHub Desktop.
ViewModels.tt - My T4 script to generate the tedious bits of WPF+MVVM+DI ViewModel types.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//<#+ | |
#if !T4 // `T4` is defined in ViewModels.tt, see line 1: `compilerOptions="/d:T4"`, ahh! | |
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
using System.IO; | |
using System.Linq; | |
using System.Text; | |
using System.Text.RegularExpressions; | |
namespace T4 | |
{ | |
static class ViewModelsTT | |
{ | |
#else | |
#endif | |
static IEnumerable<VM> GetViewModelDefinitions() | |
{ | |
yield return new VM( | |
name: "MainViewModel" | |
) { | |
Injected = { | |
new Injected( "dialogs" , "MahApps.Metro.Controls.Dialogs.IDialogCoordinator" ), | |
new Injected( "busy" , "IBusyState" ), | |
}, | |
Props = { | |
new Prop( "WindowTitle" , "String" ), | |
new Prop( "WizardPageIndex" , "Int32" ), | |
}, | |
Commands = { | |
new Cmd( "Loaded" ), | |
new Cmd( "Closing" ), | |
}, | |
ConditionalCommands = { | |
} | |
}; | |
} // end static func GetViewModelDefinitions | |
#if !T4 | |
} // end class ViewModelsTT | |
} // end namespace T4 | |
#endif | |
// #> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Linq; | |
using System.Collections.ObjectModel; | |
using GalaSoft.MvvmLight; | |
using GalaSoft.MvvmLight.CommandWpf; | |
using MyProject.Web.Client.Dtos; | |
namespace MyProject.Client.Host.Configuration.Views | |
{ | |
/// <summary>Used to unambiguously identify the T4-generated constructor.</summary> | |
internal struct GeneratedCtor | |
{ | |
} | |
/// <summary>Used to unambiguously identify a self-implemented private constructor (which is the target of pass-through parameters).</summary> | |
internal struct PrivateCtor | |
{ | |
} | |
<# foreach( VM vm in vms ) { #> | |
<# Int32 longestInjectedName = ( vm.Injected .Count == 0 ) ? 0 : vm.Injected .Max( i => i.Name.Length ); #> | |
<# Int32 longestInjectedType = ( vm.Injected .Count == 0 ) ? 0 : vm.Injected .Max( i => i.Type.Length ); #> | |
<# Int32 longestPropName = ( vm.Props .Count == 0 ) ? 0 : vm.Props .Max( p => p.Name.Length ); #> | |
<# Int32 longestImmutablePropName = ( vm.ImmutableProps .Count == 0 ) ? 0 : vm.ImmutableProps .Max( p => p.Name.Length ); #> | |
<# Int32 longestCmdName = ( vm.Commands .Count == 0 ) ? 0 : vm.Commands .Max( c => c.Name.Length ); #> | |
<# Int32 longestCmdPropertyName = ( vm.Commands .Count == 0 ) ? 0 : vm.Commands .Max( c => c.CommandPropertyName.Length ); #> | |
<# Int32 longestCondCmdName = ( vm.ConditionalCommands.Count == 0 ) ? 0 : vm.ConditionalCommands.Max( c => c.Name.Length ); #> | |
<# Int32 longestCondCmdPropertyName = ( vm.ConditionalCommands.Count == 0 ) ? 0 : vm.ConditionalCommands.Max( c => c.CommandPropertyName.Length ); #> | |
public partial class <#= vm.Name #> | |
{ | |
<# foreach( Injected inj in vm.Injected ) { #> | |
private readonly <#= inj.Type.PadRight( longestInjectedType ) #> <#= inj.Name #>; | |
<# } #> | |
#if SCAFFOLDED_CONSTRUCTORS | |
public <#= vm.Name #>(<#= vm.GetScaffoldedPublicConstructorParametersCSharp( forceWrapping: false ) #>) | |
: this(<#= vm.GetScaffoldedGeneratedConstructorParameterArgumentsCSharp( forceWrapping: false ) #>) | |
{ | |
} | |
public <#= vm.Name #>(<#= vm.GetScaffoldedPublicConstructorParametersCSharp( forceWrapping: true ) #>) | |
: this(<#= vm.GetScaffoldedGeneratedConstructorParameterArgumentsCSharp( forceWrapping: true ) #>) | |
{ | |
} | |
new <#= vm.Name #>(<#= ""/* vm.GetScaffoldedPublicConstructorParameterArguments() */ #>) | |
#endif | |
<#= vm.GeneratedCtorIsPublic ? "public" : "private" #> <#= vm.Name #>(<#= vm.GetGeneratedConstructorParametersCSharp() #>) | |
<# if( vm.PassThrough.Any() ) { #> | |
: <#= vm.PassThroughTo.ToString().ToLowerInvariant() #>(<#= vm.GetPassThroughConstructorParameterArgumentsCSharp() #>) | |
<# } #> | |
{ | |
<# if( vm.Injected.Count > 0 ) { #> | |
// Injected: | |
<# foreach( Injected inj in vm.Injected ) { #> | |
this.<#= inj.Name.PadRight( longestInjectedName ) #> = <#= inj.Name.PadRight( longestInjectedName ) #> ?? throw new ArgumentNullException( nameof(<#= inj.Name #>) ); | |
<# } #> | |
<# } #> | |
<# if( vm.ImmutableProps.Count > 0 ) { #> | |
// Immutable properties: | |
<# foreach( ImmutableProp p in vm.ImmutableProps ) { #> | |
this.<#= p.Name.PadRight( longestImmutablePropName ) #> = <#= p.NullCheck ? ( p.ParameterName.PadRight( longestImmutablePropName ) + " ?? throw new ArgumentNullException( nameof(" + p.ParameterName + ") )" ) : p.ParameterName #>; | |
<# } #> | |
<# } #> | |
<# if( vm.Commands.Any() ) { #> | |
// Commands: | |
<# foreach( Cmd cmd in vm.Commands ) { #> | |
this.<#= cmd.CommandPropertyName.PadRight( longestCmdPropertyName ) #> = new RelayCommand( this.<#= cmd.Name.PadRight( longestCmdName ) #> ); | |
<# } #> | |
<# } #> | |
<# if( vm.ConditionalCommands.Any() ) { #> | |
// Commands: | |
<# foreach( ConditionalCmd ccmd in vm.ConditionalCommands ) { #> | |
this.<#= ccmd.CommandPropertyName.PadRight( longestCondCmdPropertyName ) #> = new RelayCommand( this.<#= ccmd.Name.PadRight( longestCondCmdName ) #>, canExecute: this.<#= ccmd.CanExecuteMethodName #> ); | |
<# } #> | |
<# } #> | |
} | |
<# if( vm.Props.Any() ) { #> | |
#region INPC Properties: | |
<# foreach( Prop p in vm.Props ) { #> | |
private <#= p.Type #> <#= p.FieldName #>; | |
public <#= p.Type #> <#= p.Name #> | |
{ | |
get { return this.<#= p.FieldName #>; } | |
<# if( !vm.PropertyHasComplexSetter( p ) ) { #> | |
set { _ = this.Set( nameof(this.<#= p.Name #>), ref this.<#= p.FieldName #>, value ); } | |
<# } else { #> | |
set | |
{ | |
<#= p.Type #> oldValue = this.<#= p.FieldName #>; | |
if( this.Set( nameof(this.<#= p.Name #>), ref this.<#= p.FieldName #>, value ) ) | |
{ | |
<# foreach( XformedProp xp in vm.GetDependentXformedProps( p ) ) { #> | |
this.RaisePropertyChanged( nameof(this.<#= xp.Name #>) ); | |
<# } #> | |
<# if( p.WithOnSet && p.WithOnSetMethodName != null ) { #> | |
this.<#= p.WithOnSetMethodName #>( nameof(this.<#= p.Name #>), oldValue, value ); | |
<# } else if( p.WithOnSet ) { #> | |
this.<#= p.Name #>Changed( oldValue, value ); | |
<# } #> | |
} | |
} | |
<# } #> | |
} | |
<# if( p.WithOnSet && p.WithOnSetMethodName == null ) { #> | |
partial void <#= p.Name #>Changed(<#= p.Type #> oldValue, <#= p.Type #> newValue); | |
<# } #> | |
<# } // foreach vm.Props #> | |
#endregion /INPC Properties | |
<# } // if vm.Props.Any() #> | |
<# if( vm.ImmutableProps.Any() ) { #> | |
#region Immutable properties: | |
<# foreach( ImmutableProp p in vm.ImmutableProps ) { #> | |
public <#= p.Type #> <#= p.Name #> { get; } | |
<# } // foreach ImmutableProps #> | |
#endregion /Immutable properties | |
<# } // if vm.ImmutableProps.Any() #> | |
<# if( vm.HasAnyOnSetMethods ) { #> | |
#region OnSet methods: | |
<# foreach( String withOnSetMethodName in vm.GetOnSetMethodNames() ) { #> | |
partial void <#= withOnSetMethodName #><TValue>( String propertyName, TValue oldValue, TValue newValue); | |
<# } #> | |
#endregion /OnSet methods | |
<# } // if HasAnyOnSetMethods #> | |
<# if( vm.XformedProps.Any() ) { #> | |
#region Transformed properties: | |
<# foreach( XformedProp xp in vm.XformedProps ) { #> | |
public <#= xp.Type #> <#= xp.Name #> => <#= xp.Expression #>; | |
<# } #> | |
#endregion /Transformed properties | |
<# } // if vm.XformedProps.Any() #> | |
<# if( vm.Collections.Any() ) { #> | |
#region Collection properties: | |
<# foreach( Col c in vm.Collections ) { #> | |
public ObservableCollection<<#= c.Type #>> <#= c.Name #> { get; } = new ObservableCollection<<#= c.Type #>>(); | |
<# } #> | |
#endregion /Collection properties | |
<# } // if vm.Collections.Any() #> | |
<# if( vm.Commands.Any() ) { #> | |
#region Commands: | |
<# foreach( Cmd cmd in vm.Commands ) { #> | |
public RelayCommand <#= cmd.CommandPropertyName #> { get; } | |
partial void <#= cmd.Name #>(); | |
<# } #> | |
#endregion /Commands | |
<# } // if vm.Commands.Any() #> | |
<# if( vm.ConditionalCommands.Any() ) { #> | |
#region Conditional commands: | |
<# foreach( ConditionalCmd cmd in vm.ConditionalCommands ) { #> | |
public RelayCommand <#= cmd.CommandPropertyName #> { get; } | |
partial void <#= cmd.Name #>(); | |
private Boolean <#= cmd.EnabledFieldName #><#= cmd.IsEnabledByDefault ? " = true" : "" #>; | |
private Boolean <#= cmd.CanExecuteMethodName #>() => this.<#= cmd.EnabledPropertyName #>; | |
public Boolean <#= cmd.EnabledPropertyName #> | |
{ | |
get { return this.<#= cmd.EnabledFieldName #>; } | |
set | |
{ | |
if( this.Set( nameof(this.<#= cmd.EnabledPropertyName #>), ref this.<#= cmd.EnabledFieldName #>, value ) ) | |
{ | |
this.<#= cmd.CommandPropertyName #>.RaiseCanExecuteChanged(); | |
} | |
} | |
} | |
<# } #> | |
#endregion Conditional commands | |
<# } // if vm.ConditionalCommands.Any() #> | |
} // VM class | |
<# } #> | |
} | |
<#+ | |
#> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<#@ template debug="false" hostspecific="false" language="C#" compilerOptions="/d:T4" #> | |
<#@ assembly name="System.Core" #> | |
<#@ import namespace="System.Linq" #> | |
<#@ import namespace="System.Text" #> | |
<#@ import namespace="System.Collections.Generic" #> | |
<#@ output extension=".cs" #> | |
<#@ include file="ViewModels.types.cs" #> | |
<#@ include file="ViewModels.definitions.cs" #> | |
<# | |
List<VM> vms = GetViewModelDefinitions().ToList(); | |
#> | |
<#@ Include File="ViewModels.render.ttinclude" #> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//<#+ | |
// See my answer in https://stackoverflow.com/questions/24957737/t4-tt-using-custom-classes-in-tt-files/58760240#58760240 | |
#if !T4 // `T4` is defined in ViewModels.tt, see line 1: `compilerOptions="/d:T4"`, ahh! | |
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
using System.IO; | |
using System.Linq; | |
using System.Text; | |
using System.Text.RegularExpressions; | |
namespace T4 | |
{ | |
#endif | |
enum PassThroughTo | |
{ | |
/// <summary><c>base(...)</c></summary> | |
This, | |
/// <summary><c>this(...)</c>.</summary> | |
Base | |
} | |
class VM { | |
public VM( | |
String name, | |
Boolean generatedCtorIsPublic = false, | |
PassThroughTo passThroughTo = PassThroughTo.This | |
) | |
{ | |
this.Name = name; | |
this.GeneratedCtorIsPublic = generatedCtorIsPublic; | |
this.PassThroughTo = passThroughTo; | |
} | |
public String Name { get; } | |
public Boolean GeneratedCtorIsPublic { get; } | |
public PassThroughTo PassThroughTo { get; } | |
// | |
public List<Injected> Injected { get; } = new List<Injected>(); | |
public List<PassThrough> PassThrough { get; } = new List<PassThrough>(); | |
public List<Prop> Props { get; } = new List<Prop>(); | |
public List<ImmutableProp> ImmutableProps { get; } = new List<ImmutableProp>(); | |
public List<XformedProp> XformedProps { get; } = new List<XformedProp>(); | |
public List<Cmd> Commands { get; } = new List<Cmd>(); | |
public List<ConditionalCmd> ConditionalCommands { get; } = new List<ConditionalCmd>(); | |
public List<Col> Collections { get; } = new List<Col>(); | |
// | |
public Boolean PropertyHasComplexSetter( Prop p ) | |
{ | |
Boolean complexSetter = p.WithOnSet || this.XformedProps.Any( tp => tp.DependsOn.Contains( p.Name ) ); | |
return complexSetter; | |
} | |
public IEnumerable<XformedProp> GetDependentXformedProps( Prop p ) | |
{ | |
return this.XformedProps | |
.Where( tp => tp.DependsOn.Contains( p.Name ) ); | |
} | |
public Boolean HasAnyOnSetMethods => this.Props.Any( p => p.WithOnSetMethodName != null ); | |
public IEnumerable<String> GetOnSetMethodNames() | |
{ | |
return this.Props | |
.Where( p => p.WithOnSetMethodName != null ) | |
.Select( p => p.WithOnSetMethodName ) | |
.Distinct() | |
.OrderBy( s => s ) | |
.ToList(); | |
} | |
#region Constructor parameter and argument rendering | |
private IEnumerable<CtorParam> GetConstructorParams() | |
{ | |
foreach( Injected injected in this.Injected ) | |
{ | |
yield return new CtorParam( type: injected.Type, name: injected.Name, isPassThrough: false ); | |
} | |
foreach( ImmutableProp immutableProperty in this.ImmutableProps ) | |
{ | |
yield return new CtorParam( type: immutableProperty.Type, name: immutableProperty.ParameterName, isPassThrough: false ); | |
} | |
foreach( PassThrough passThrough in this.PassThrough ) | |
{ | |
yield return new CtorParam( type: passThrough.Type, name: passThrough.Name, isPassThrough: true ); | |
} | |
} | |
private enum RenderMode | |
{ | |
Definition, | |
CallSite | |
} | |
private static String RenderParams( IReadOnlyCollection<CtorParam> parameters, RenderMode mode, Boolean forceWrapping = false ) | |
{ | |
if( parameters.Count == 0 ) | |
{ | |
return ""; | |
} | |
else if( parameters.Count <= 3 && forceWrapping == false ) // Render them on the same line (and without name padding): | |
{ | |
IEnumerable<String> renderedParams; | |
if( mode == RenderMode.Definition ) | |
{ | |
renderedParams = parameters.Select( p => p.GetDefinitionCSharp( longestTypeName: 0, rightPadName: 0 ) ); | |
} | |
else // RenderMode.CallSite | |
{ | |
renderedParams = parameters.Select( p => p.GetNamedCallSiteCSharp( longestCamelCaseName: 0, localName: null ) ); | |
} | |
return " " + String.Join( separator: ", ", renderedParams ) + " "; | |
} | |
else // Render each on their own line: | |
{ | |
Int32 longestType = parameters.Max( p => p.Type.Length ); | |
Int32 longestName = parameters.Max( p => p.Name.Length ); | |
String prefix; | |
String separator; | |
IEnumerable<String> renderedParams; | |
String suffix; | |
if( mode == RenderMode.Definition ) | |
{ | |
prefix = "\r\n\t\t\t"; | |
separator = ",\r\n\t\t\t"; | |
renderedParams = parameters.Select( p => p.GetDefinitionCSharp( longestTypeName: longestType, rightPadName: 0 ) ); | |
suffix = "\r\n\t\t"; | |
} | |
else // RenderMode.CallSite | |
{ | |
prefix = "\r\n\t\t\t\t"; | |
separator = ",\r\n\t\t\t\t"; | |
renderedParams = parameters.Select( p => p.GetNamedCallSiteCSharp( longestCamelCaseName: longestName, localName: null ) ); | |
suffix = "\r\n\t\t\t"; | |
} | |
// | |
return prefix + String.Join( separator: separator, renderedParams ) + suffix; | |
} | |
} | |
public String GetGeneratedConstructorParametersCSharp() | |
{ | |
List<CtorParam> parameters = new List<CtorParam>(); | |
if( this.GeneratedCtorIsPublic == false ) | |
{ | |
parameters.Add( new CtorParam( type: "GeneratedCtor", name: "generatedCtor", isPassThrough: false ) ); | |
} | |
parameters.AddRange( this.GetConstructorParams() ); | |
return RenderParams( parameters, mode: RenderMode.Definition ); | |
} | |
public String GetPassThroughConstructorParameterArgumentsCSharp() | |
{ | |
List<CtorParam> parameters = new List<CtorParam>(); | |
if( this.PassThroughTo == PassThroughTo.This ) | |
{ | |
parameters.Add( new CtorParam( type: "PrivateCtor", name: "privateCtor", isPassThrough: true ) ); | |
} | |
parameters.AddRange( this.GetConstructorParams().Where( p => p.IsPassThrough ) ); | |
return RenderParams( parameters, mode: RenderMode.CallSite ); | |
} | |
#region Scaffolding | |
public String GetScaffoldedPublicConstructorParametersCSharp( Boolean forceWrapping ) | |
{ | |
List<CtorParam> parameters = new List<CtorParam>(); | |
parameters.AddRange( this.GetConstructorParams() ); | |
return RenderParams( parameters, mode: RenderMode.Definition, forceWrapping: forceWrapping ).Replace( ": generatedCtor", ": default" ); | |
} | |
public String GetScaffoldedGeneratedConstructorParameterArgumentsCSharp( Boolean forceWrapping ) | |
{ | |
List<CtorParam> parameters = new List<CtorParam>(); | |
if( this.GeneratedCtorIsPublic == false ) | |
{ | |
parameters.Add( new CtorParam( type: "GeneratedCtor", name: "generatedCtor", isPassThrough: false ) ); | |
} | |
parameters.AddRange( this.GetConstructorParams() ); | |
return RenderParams( parameters, mode: RenderMode.CallSite, forceWrapping: forceWrapping ).Replace( ": generatedCtor", ": default" ); | |
} | |
#endregion | |
#endregion | |
} | |
/// <summary>Constructor parameter.</summary> | |
class CtorParam { | |
/// <summary></summary> | |
/// <param name="type"></param> | |
/// <param name="name">Camel-case name.</param> | |
internal CtorParam( String type, String name, Boolean isPassThrough ) { | |
this.Type = type; | |
this.Name = name; | |
this.IsPassThrough = isPassThrough; | |
} | |
/// <summary>Type-name.</summary> | |
public String Type { get; } | |
/// <summary>Camel-case name.</summary> | |
public String Name { get; } | |
public Boolean IsPassThrough { get; } | |
// public String GetNamedCallSiteCSharp( Int32 longestCamelCaseName, String localName ) { | |
// if( localName != null ) { | |
// return this.Name.PadRight( longestCamelCaseName ) + ": " + localName; | |
// } | |
// else { | |
// return localName; | |
// } | |
// } | |
public String GetNamedCallSiteCSharp( Int32 longestCamelCaseName, String localName ) { | |
if( localName is null ) localName = this.Name; | |
return this.Name.PadRight( longestCamelCaseName ) + ": " + localName; | |
} | |
public String GetDefinitionCSharp( Int32 longestTypeName, Int32 rightPadName ) { | |
return this.Type.PadRight( longestTypeName ) + " " + this.Name.PadRight( rightPadName ); | |
} | |
} | |
/// <summary>An injected DI service. These are saved to <c>private readonly</c> instance fields.</summary> | |
class Injected { | |
public Injected(String name, String type) { | |
this.Name = name; | |
this.Type = type; | |
} | |
public String Name { get; } | |
public String Type { get; } | |
} | |
/// <summary>A ctor parameter that is passed-on to another ctor.</summary> | |
class PassThrough { | |
public PassThrough(String name, String type) { | |
this.Name = name; | |
this.Type = type; | |
} | |
/// <summary>camelCase</summary> | |
public String Name { get; } | |
public String Type { get; } | |
} | |
/// <summary>A "normal" NotifyPropertyChanged property.</summary> | |
class Prop { | |
public Prop(String name, String type, String initialExpression = null) | |
: this( name, type, false ) { | |
} | |
public Prop(String name, String type, Boolean withOnSet, String initialExpression = null, String withOnSetMethodName = null) { | |
this.Name = name; | |
this.Type = type; | |
this.InitialExpression = initialExpression; | |
this.FieldName = Naming.ToCamelCase( name ) + "_Field"; // The `_Field` suffix is to reduce ambiguity when the only difference is casing. | |
this.WithOnSet = withOnSet; | |
this.WithOnSetMethodName = withOnSetMethodName; | |
} | |
public String Name { get; set; } | |
public String Type { get; set; } | |
public String InitialExpression { get; set; } | |
public Boolean WithOnSet { get; set; } | |
public String WithOnSetMethodName { get; set; } | |
public String FieldName { get; set; } | |
} | |
/// <summary>An immutable class instance property.</summary> | |
class ImmutableProp { | |
/// <summary></summary> | |
/// <param name="name">PascalCase</param> | |
/// <param name="type"></param> | |
public ImmutableProp( String name, String type ) | |
{ | |
this.Name = name ?? throw new ArgumentNullException(nameof(name)); | |
this.Type = type ?? throw new ArgumentNullException(nameof(type)); | |
if( type.StartsWith( "enum:" ) ) | |
{ | |
this.Type = type.Substring( startIndex: 5 ); | |
this.NullCheck = false; | |
} | |
else if( type.EndsWith( "?" ) ) | |
{ | |
this.NullCheck = false; | |
} | |
else if( IsValueType( type ) ) | |
{ | |
this.NullCheck = false; | |
} | |
else | |
{ | |
this.NullCheck = true; | |
} | |
this.ParameterName = Char.ToLower( this.Name[0] ) + this.Name.Substring( 1 ); | |
} | |
private static Boolean IsValueType( String type ) | |
{ | |
switch( type ) | |
{ | |
case "SByte" : case "Byte" : | |
case "Int16" : case "UInt16": | |
case "Int32" : case "UInt32": | |
case "Int64" : case "UInt64": | |
case "Single": case "Double": case "Decimal": | |
return true; | |
default: | |
return false; | |
} | |
} | |
/// <summary>PascalCase</summary> | |
public String Name { get; } | |
public String Type { get; } | |
public Boolean NullCheck { get; } | |
/// <summary>Camel-case</summary> | |
public String ParameterName { get; } | |
} | |
/// <summary>"Transformed property": a property that is a transformation to/from 1 or more other properties.</summary> | |
class XformedProp { | |
public XformedProp( String name, String type, String expr, params String[] dependsOn ) { | |
this.Name = name; | |
this.Type = type; | |
this.DependsOn = new HashSet<String>( dependsOn ); | |
this.Expression = expr; | |
} | |
/// <summary>PascalCase</summary> | |
public String Name { get; set; } | |
public String Type { get; set; } | |
public HashSet<String> DependsOn { get; set; } | |
public String Expression { get; set; } | |
} | |
/// <summary>A collection property. These are rendered to <c>public ObservableCollection<<#= c.Type #>> <#= c.Name #> { get; } = new ObservableCollection<<#= c.Type #>>();</c></summary> | |
class Col { | |
public Col(String name, String type) { | |
this.Name = name; | |
this.Type = type; | |
} | |
public String Name { get; set; } | |
public String Type { get; set; } | |
} | |
/// <summary>A simple <c>RelayCommand</c> property bound to a private method. The command is always enabled. These are rendered as:<br /> | |
class Cmd { | |
public Cmd(String name) { | |
this.Name = name; | |
this.CommandPropertyName = name + "Command"; | |
} | |
public String Name { get; set; } | |
public String CommandPropertyName { get; set; } | |
} | |
class ConditionalCmd : Cmd { | |
/// <summary>When using this ctor, a bool INPC property will be created with the name <c><paramref name="name"/> + "Enabled"</c></summary> | |
/// <param name="name">PascalCase</param> | |
/// <param name="isEnabledByDefault"></param> | |
public ConditionalCmd(String name, Boolean isEnabledByDefault) | |
: base( name: name ) | |
{ | |
this.IsEnabledByDefault = isEnabledByDefault; | |
this.EnabledPropertyName = name + "Enabled"; | |
this.EnabledFieldName = Char.ToLower( this.EnabledPropertyName[0] ) + this.EnabledPropertyName.Substring(1); | |
this.CanExecuteMethodName = this.Name + "CanExecute"; | |
} | |
/// <summary></summary> | |
/// <param name="name">PascalCase</param> | |
/// <param name="propertyName">The name of the <see cref="BooleanSwitch"/> INPC property that indicates if this command is enabled or not.</param> | |
/// <param name="isEnabledByDefault"></param> | |
public ConditionalCmd(String name, String propertyName) | |
: base( name: name ) | |
{ | |
this.EnabledPropertyName = propertyName; | |
this.CanExecuteMethodName = this.Name + "CanExecute"; | |
} | |
public Boolean IsEnabledByDefault { get; set; } | |
public String EnabledFieldName { get; set; } | |
public String EnabledPropertyName { get; set; } | |
/// <summary>NOTE: A CanExecute method (not a property) is always generated because RelayCommand expects a <see cref="Func{T, TResult}"/>. Passing in a lambda/closure risks causing leaks - so this is safer.</summary> | |
public String CanExecuteMethodName { get; } | |
} | |
internal static class Naming | |
{ | |
public static String ToPascalCase( String camelCase ) | |
{ | |
if( camelCase is null ) throw new ArgumentNullException(nameof(camelCase)); | |
if( String.IsNullOrWhiteSpace(camelCase) ) throw new ArgumentException( message: "Value cannot be empty or whitespace.", paramName: nameof(camelCase)); | |
camelCase = camelCase.Trim(); | |
if( Char.IsUpper(camelCase[0]) ) throw new ArgumentException( message: "Camelcase value cannot start with an uppercase character: \"" + camelCase + "\".", paramName: nameof(camelCase)); | |
// | |
if( camelCase.Length == 1 ) | |
{ | |
return camelCase.ToUpperInvariant(); | |
} | |
else if( camelCase.Length == 2 ) | |
{ | |
if( Char.IsUpper( camelCase[1] ) ) | |
{ | |
// e.g. "aB". | |
return camelCase.ToUpperInvariant(); | |
} | |
else | |
{ | |
// e.g. "ab", "io" | |
if( IsTwoLetterInitialism( camelCase ) ) | |
{ | |
// e.g. "io" -> "IO" | |
return camelCase.ToUpperInvariant(); | |
} | |
else | |
{ | |
// e.g. "ab" -> "Ab" | |
return Char.ToUpperInvariant( camelCase[0] ).ToString() + camelCase.Substring( startIndex: 1 ); | |
} | |
} | |
} | |
else // Implicit: camelCase.Length >= 3 | |
{ | |
if( Char.IsLower( camelCase[1] ) ) | |
{ | |
// e.g. "abc" -> "Abc" | |
// e.g. "abC" -> "AbC" | |
return Char.ToUpperInvariant( camelCase[0] ).ToString() + camelCase.Substring( startIndex: 1 ); | |
} | |
else | |
{ | |
// e.g. "aBc" -> "ABc" | |
// e.g. "aBC" -> "ABC" | |
return Char.ToUpperInvariant( camelCase[0] ).ToString() + camelCase.Substring( startIndex: 1 ); | |
} | |
} | |
} | |
private static Boolean IsTwoLetterInitialism( String text ) | |
{ | |
text = text.ToUpperInvariant(); | |
switch( text ) | |
{ | |
case "IO": | |
return true; | |
default: | |
return false; | |
} | |
} | |
public static String ToCamelCase( String pascalCase ) | |
{ | |
if( pascalCase is null ) throw new ArgumentNullException(nameof(pascalCase)); | |
if( String.IsNullOrWhiteSpace(pascalCase) ) throw new ArgumentException( message: "Value cannot be empty or whitespace.", paramName: nameof(pascalCase)); | |
pascalCase = pascalCase.Trim(); | |
if( Char.IsLower(pascalCase[0]) ) throw new ArgumentException( message: "pascalCase value cannot start with a lowercase character: \"" + pascalCase + "\".", paramName: nameof(pascalCase)); | |
// | |
if( pascalCase.Length == 1 ) | |
{ | |
return pascalCase.ToLowerInvariant(); | |
} | |
else if( pascalCase.Length == 2 ) | |
{ | |
if( Char.IsUpper( pascalCase[1] ) ) | |
{ | |
// e.g. "AB", "IO" | |
if( IsTwoLetterInitialism( pascalCase ) ) | |
{ | |
// e.g. "IO" -> "io" | |
return pascalCase.ToLowerInvariant(); | |
} | |
else | |
{ | |
// e.g. "AB" -> "aB" | |
return Char.ToLowerInvariant( pascalCase[0] ).ToString() + pascalCase.Substring( startIndex: 1 ); | |
} | |
} | |
else | |
{ | |
// e.g. "Ab" -> "ab" | |
// e.g. "Io" -> "io" | |
return pascalCase.ToLowerInvariant(); | |
} | |
} | |
else // Implicit: pascalCase.Length >= 3 | |
{ | |
if( Char.IsLower( pascalCase[1] ) ) | |
{ | |
// e.g. "Abc" -> "abc" | |
// e.g. "AbC" -> "abC" | |
// e.g. "IoC" -> "ioC" // 'O' wasn't already capitalized in the PascalCase input, therefore "Io" is not an initialism. | |
return Char.ToLowerInvariant( pascalCase[0] ).ToString() + pascalCase.Substring( startIndex: 1 ); | |
} | |
else // Implicit: Char.IsUpper( pascalCase[1] ) | |
{ | |
// e.g. "ABc" -> "aBc" | |
// e.g. "ABC" -> "ABC" | |
// e.g. "ABCD" -> "abCD" | |
// e.g. "ABCd" -> "abCd" | |
// e.g. "IOC" -> "ioC" | |
// e.g. "IOC" -> "ioC" | |
if( Char.IsUpper( pascalCase[2] ) ) | |
{ | |
// e.g. "ABC" -> "abC" | |
// e.g. "AIO" -> "aIO" | |
if( IsTwoLetterInitialism( pascalCase.Substring( startIndex: 0, length: 2 ) ) ) | |
{ | |
// e.g. "IOC" -> "ioC" | |
return pascalCase.Substring( startIndex: 0, length: 2 ).ToLowerInvariant() + pascalCase.Substring( startIndex: 2 ); | |
} | |
else | |
{ | |
if( IsTwoLetterInitialism( pascalCase.Substring( startIndex: 1, length: 2 ) ) ) | |
{ | |
// e.g. "AIO" -> "aIO" | |
return Char.ToLowerInvariant( pascalCase[0] ).ToString() + pascalCase.Substring( startIndex: 1 ); | |
} | |
else | |
{ | |
// e.g. "ABC" -> "aBC", such that "AB" is not an initialism | |
return Char.ToLowerInvariant( pascalCase[0] ).ToString() + pascalCase.Substring( startIndex: 1 ); | |
} | |
} | |
} | |
else | |
{ | |
// e.g. "ABc" -> "aBc" | |
return Char.ToLowerInvariant( pascalCase[0] ).ToString() + pascalCase.Substring( startIndex: 1 ); | |
} | |
} | |
} | |
} | |
} | |
#if !T4 | |
} // end namespace T4 | |
#endif | |
// #> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment