Skip to content

Instantly share code, notes, and snippets.

@daiplusplus
Created April 18, 2021 02:44
Show Gist options
  • Save daiplusplus/844ae242025e0f0b750503629d46087e to your computer and use it in GitHub Desktop.
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.
//<#+
#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
// #>
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
<# } #>
}
<#+
#>
<#@ 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" #>
//<#+
// 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 &quot;normal&quot; 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>&quot;Transformed property&quot;: 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&lt;&lt;#= c.Type #&gt;&gt; &lt;#= c.Name #&gt; { get; } = new ObservableCollection&lt;&lt;#= c.Type #&gt;&gt;();</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"/> + &quot;Enabled&quot;</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