From bcc66e59bfa81182cd030c3020ba4741056c25bc Mon Sep 17 00:00:00 2001 From: Rob Moore Date: Sat, 16 Jul 2016 23:17:29 +0800 Subject: [PATCH 1/6] Bumping major version in preparation for adding flags enum support --- BREAKING_CHANGES.md | 13 +++++++++++++ .../Component/Config/FieldConfiguration.cs | 18 +----------------- .../Config/ReadonlyFieldConfiguration.cs | 9 ++------- GitVersionConfig.yaml | 2 +- 4 files changed, 17 insertions(+), 25 deletions(-) diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index 3d0e82f7..e59ad9b8 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -1,6 +1,19 @@ ChameleonForms Breaking Changes ------------------------------- +Version 3.0.0 +============= + +Enums marked with the `[Flags]` attribute will now show as multiple select elements (or checkboxes when displaying as a list). + +### Reason + +Support has been added for flags enums; these make sense as multiple-select controls since a flags enum can support multiple values. + +### Workaround + +Create a enum class without the `[Flags]` attribute with the same values and bind to that instead. + Version 2.0.0 ============= diff --git a/ChameleonForms/Component/Config/FieldConfiguration.cs b/ChameleonForms/Component/Config/FieldConfiguration.cs index de030462..707cf723 100644 --- a/ChameleonForms/Component/Config/FieldConfiguration.cs +++ b/ChameleonForms/Component/Config/FieldConfiguration.cs @@ -254,14 +254,7 @@ public interface IFieldConfiguration : IHtmlString, IReadonlyFieldConfiguration /// /// The to allow for method chaining IFieldConfiguration HideEmptyItem(); - - /// - /// Don't use a <label>, but still include the label text for the field. - /// - /// The to allow for method chaining - [Obsolete("Use WithoutLabelElementElement alias instead")] - IFieldConfiguration WithoutLabel(); - + /// /// Don't use a <label>, but still include the label text for the field. /// @@ -614,15 +607,6 @@ public IFieldConfiguration WithoutLabelElement() return this; } - /// - public IFieldConfiguration WithoutLabel() - { - return this.WithoutLabelElement(); - } - - /// - public bool HasLabel { get { return this.HasLabelElement; } } - /// public bool HasLabelElement { get; private set; } diff --git a/ChameleonForms/Component/Config/ReadonlyFieldConfiguration.cs b/ChameleonForms/Component/Config/ReadonlyFieldConfiguration.cs index 19458d52..640221d3 100644 --- a/ChameleonForms/Component/Config/ReadonlyFieldConfiguration.cs +++ b/ChameleonForms/Component/Config/ReadonlyFieldConfiguration.cs @@ -88,17 +88,12 @@ public interface IReadonlyFieldConfiguration /// Whether or not the empty item is hidden. /// bool EmptyItemHidden { get; } - - /// - /// Whether or not to use a <label>. - /// - [Obsolete("Use HasLabelElement alias instead")] - bool HasLabel { get; } - + /// /// Whether or not to use a <label>. /// bool HasLabelElement { get; } + /// /// Any CSS class(es) to use for the field label. /// diff --git a/GitVersionConfig.yaml b/GitVersionConfig.yaml index 3bca6f1e..9aed8a8a 100644 --- a/GitVersionConfig.yaml +++ b/GitVersionConfig.yaml @@ -1,3 +1,3 @@ mode: ContinuousDelivery -next-version: 2.1.0 +next-version: 3.0.0 branches: {} From b37094b2971299715ae243c6a30b05ed1cfac731 Mon Sep 17 00:00:00 2001 From: Rob Moore Date: Sat, 16 Jul 2016 23:36:36 +0800 Subject: [PATCH 2/6] Added support for flags enums todo: documentation updates --- .gitignore | 1 + AppStart.cs.pp | 6 + .../Helpers/ObjectMother.cs | 3 + .../ModelBinding/ModelBindingTests.cs | 1 - .../Pages/Fields/MultipleFields.cs | 12 +- .../ModelBinding/Pages/ModelFieldType.cs | 38 +++++- .../ModelBinding/Pages/ModelFieldValue.cs | 24 +++- .../Controllers/ExampleFormsController.cs | 18 +++ ChameleonForms.Example/Global.asax.cs | 8 ++ .../Views/ExampleForms/Form1.cshtml | 1 + .../ExampleForms/ModelBindingExample.cshtml | 3 + .../RequiredFlagsEnumAttributeTests.cs | 41 ++++++ .../ChameleonForms.Tests.csproj | 3 + .../Config/FieldConfigurationTests.cs | 2 +- ...ld_viewmodel_enum_list_field.approved.html | 14 ++ ...num_list_with_excluded_value.approved.html | 4 + ..._for_label_for_enum_dropdown.approved.html | 1 + ...html_for_label_for_enum_list.approved.html | 1 + ...m_list_with_overridden_label.approved.html | 1 + ...nullable_required_enum_field.approved.html | 5 + ...html_for_optional_enum_field.approved.html | 6 + ...d_with_null_string_attribute.approved.html | 6 + ...ridden_null_string_attribute.approved.html | 6 + ...html_for_required_enum_field.approved.html | 5 + .../DefaultFieldGenerator/FlagsEnumTests.cs | 110 +++++++++++++++ .../DefaultFieldGeneratorTests.cs | 105 +++++++++------ .../FlagsEnumModelBinderShould.cs | 115 ++++++++++++++++ ChameleonForms.sln.DotSettings | 1 + .../Attributes/RequiredFlagsEnumAttribute.cs | 20 +++ ChameleonForms/ChameleonForms.csproj | 4 +- .../Handlers/BooleanHandler.cs | 3 +- .../Handlers/FieldGeneratorHandler.cs | 127 ++++++++++++++---- .../ModelBinders/FlagsEnumModelBinder.cs | 54 ++++++++ ChameleonForms/Templates/HtmlCreator.cs | 37 ++++- 34 files changed, 709 insertions(+), 77 deletions(-) create mode 100644 ChameleonForms.Tests/Attributes/RequiredFlagsEnumAttributeTests.cs create mode 100644 ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_child_viewmodel_enum_list_field.approved.html create mode 100644 ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_enum_list_with_excluded_value.approved.html create mode 100644 ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_label_for_enum_dropdown.approved.html create mode 100644 ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_label_for_enum_list.approved.html create mode 100644 ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_label_for_enum_list_with_overridden_label.approved.html create mode 100644 ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_nullable_required_enum_field.approved.html create mode 100644 ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_optional_enum_field.approved.html create mode 100644 ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_optional_enum_field_with_null_string_attribute.approved.html create mode 100644 ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_optional_enum_field_with_overridden_null_string_attribute.approved.html create mode 100644 ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_required_enum_field.approved.html create mode 100644 ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.cs create mode 100644 ChameleonForms.Tests/ModelBinders/FlagsEnumModelBinderShould.cs create mode 100644 ChameleonForms/Attributes/RequiredFlagsEnumAttribute.cs create mode 100644 ChameleonForms/ModelBinders/FlagsEnumModelBinder.cs diff --git a/.gitignore b/.gitignore index f2e847e6..f48e7c9b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ _ReSharper.* *.log packages *.received.* +.vs diff --git a/AppStart.cs.pp b/AppStart.cs.pp index db622e21..8ff90bbc 100644 --- a/AppStart.cs.pp +++ b/AppStart.cs.pp @@ -12,6 +12,12 @@ { System.Web.Mvc.ModelBinders.Binders.Add(typeof(DateTime), new DateTimeModelBinder()); System.Web.Mvc.ModelBinders.Binders.Add(typeof(DateTime?), new DateTimeModelBinder()); + GetType().Assembly.GetTypes().Where(t => t.IsEnum && t.GetCustomAttributes(typeof(FlagsAttribute), false).Any()) + .ToList().ForEach(t => + { + System.Web.Mvc.ModelBinders.Binders.Add(t, new FlagsEnumModelBinder()); + System.Web.Mvc.ModelBinders.Binders.Add(typeof(Nullable<>).MakeGenericType(t), new FlagsEnumModelBinder()); + }); } } } diff --git a/ChameleonForms.AcceptanceTests/Helpers/ObjectMother.cs b/ChameleonForms.AcceptanceTests/Helpers/ObjectMother.cs index 751c5cce..cda3b97f 100644 --- a/ChameleonForms.AcceptanceTests/Helpers/ObjectMother.cs +++ b/ChameleonForms.AcceptanceTests/Helpers/ObjectMother.cs @@ -25,6 +25,9 @@ public static ModelBindingViewModel BasicValid RequiredEnum = SomeEnum.SomeOtherValue, RequiredNullableEnum = SomeEnum.Value1, OptionalEnum = null, + RequiredFlagsEnum = FlagsEnum.Four | FlagsEnum.Two, + RequiredNullableFlagsEnum = FlagsEnum.Two | FlagsEnum.Five, + OptionalFlagsEnum = null, RequiredEnums = new List { SomeEnum.ValueWithDescription, SomeEnum.SomeOtherValue }, RequiredNullableEnums = new List { SomeEnum.Value1 }, OptionalEnums = null, diff --git a/ChameleonForms.AcceptanceTests/ModelBinding/ModelBindingTests.cs b/ChameleonForms.AcceptanceTests/ModelBinding/ModelBindingTests.cs index 03af1cdc..b029f7b3 100644 --- a/ChameleonForms.AcceptanceTests/ModelBinding/ModelBindingTests.cs +++ b/ChameleonForms.AcceptanceTests/ModelBinding/ModelBindingTests.cs @@ -1,7 +1,6 @@ using ChameleonForms.AcceptanceTests.Helpers; using ChameleonForms.AcceptanceTests.ModelBinding.Pages; using NUnit.Framework; -using TestStack.Seleno.Configuration; namespace ChameleonForms.AcceptanceTests.ModelBinding { diff --git a/ChameleonForms.AcceptanceTests/ModelBinding/Pages/Fields/MultipleFields.cs b/ChameleonForms.AcceptanceTests/ModelBinding/Pages/Fields/MultipleFields.cs index 7dc006cd..a7a2f485 100644 --- a/ChameleonForms.AcceptanceTests/ModelBinding/Pages/Fields/MultipleFields.cs +++ b/ChameleonForms.AcceptanceTests/ModelBinding/Pages/Fields/MultipleFields.cs @@ -25,9 +25,17 @@ public object Get(IModelFieldType fieldType) { var values = _elements .Select(e => FieldFactory.Create(new[] {e}).Get(new ModelFieldType(fieldType.BaseType, fieldType.Format))) - .Where(e => e != null); + .Where(e => e != null) + .ToArray(); - if (fieldType.HasMultipleValues) + if (fieldType.IsFlagsEnum) + { + if (values.Length == 1) + return values.First(); + return fieldType.GetValueFromStrings(values.Select(v => v.ToString())); + } + + if (fieldType.IsEnumerable) return fieldType.Cast(values); return values.FirstOrDefault() ?? fieldType.DefaultValue; diff --git a/ChameleonForms.AcceptanceTests/ModelBinding/Pages/ModelFieldType.cs b/ChameleonForms.AcceptanceTests/ModelBinding/Pages/ModelFieldType.cs index f0760e08..d905dca7 100644 --- a/ChameleonForms.AcceptanceTests/ModelBinding/Pages/ModelFieldType.cs +++ b/ChameleonForms.AcceptanceTests/ModelBinding/Pages/ModelFieldType.cs @@ -16,6 +16,8 @@ internal interface IModelFieldType bool HasMultipleValues { get; } bool IsBoolean { get; } Type BaseType { get; } + bool IsFlagsEnum { get; } + bool IsEnumerable { get; } } internal class ModelFieldType : IModelFieldType @@ -34,7 +36,7 @@ public ModelFieldType(Type fieldType, string format) public object GetValueFromString(string stringValue) { var value = GetUnderlyingValueFromString(stringValue); - if (!HasMultipleValues) + if (!IsEnumerable) return value; return Cast(new[] {value}); @@ -48,6 +50,19 @@ public object GetValueFromStrings(IEnumerable stringValues) if (!HasMultipleValues) return GetUnderlyingValueFromString(string.Join(",", stringValues.Where(s => !string.IsNullOrEmpty(s)))); + if (IsFlagsEnum) + { + var value = Enum.Parse(UnderlyingType, stringValues + .Where(v => !string.IsNullOrEmpty(v)) + .Select(v => Convert.ToInt64(Enum.Parse(UnderlyingType, v))) + .Aggregate(0L, (x, y) => x | y).ToString()); + + if (Convert.ToInt64(value) == 0L && Nullable.GetUnderlyingType(BaseType) != null) + return null; + + return value; + } + return Cast(stringValues.Where(s => !string.IsNullOrEmpty(s)).Select(GetUnderlyingValueFromString)); } @@ -94,11 +109,25 @@ public Type UnderlyingType } public bool HasMultipleValues + { + get { return IsEnumerable || IsFlagsEnum; } + } + + public bool IsFlagsEnum + { + get + { + return UnderlyingType.IsEnum + && UnderlyingType.GetCustomAttributes(typeof(FlagsAttribute), false).Any(); + } + } + + public bool IsEnumerable { get { return _fieldType.IsGenericType && - typeof (IEnumerable<>).IsAssignableFrom(_fieldType.GetGenericTypeDefinition()); + typeof(IEnumerable<>).IsAssignableFrom(_fieldType.GetGenericTypeDefinition()); } } @@ -111,7 +140,10 @@ public Type BaseType { get { - return HasMultipleValues ? _fieldType.GetGenericArguments()[0] : _fieldType; + if (_fieldType.IsGenericType && + typeof(IEnumerable<>).IsAssignableFrom(_fieldType.GetGenericTypeDefinition())) + return _fieldType.GetGenericArguments()[0]; + return _fieldType; } } } diff --git a/ChameleonForms.AcceptanceTests/ModelBinding/Pages/ModelFieldValue.cs b/ChameleonForms.AcceptanceTests/ModelBinding/Pages/ModelFieldValue.cs index b1c60839..816be325 100644 --- a/ChameleonForms.AcceptanceTests/ModelBinding/Pages/ModelFieldValue.cs +++ b/ChameleonForms.AcceptanceTests/ModelBinding/Pages/ModelFieldValue.cs @@ -24,7 +24,21 @@ public ModelFieldValue(object value, string format) _format = format; } - public bool HasMultipleValues { get { return _value as IEnumerable != null && _value.GetType() != typeof(string); } } + public bool HasMultipleValues + { + get + { + if (_value == null) + return false; + + var underlyingType = Nullable.GetUnderlyingType(_value.GetType()) ?? _value.GetType(); + if (underlyingType.IsEnum && underlyingType.GetCustomAttributes(typeof(FlagsAttribute), false).Any()) + return true; + + return _value is IEnumerable + && _value.GetType() != typeof(string); + } + } public IEnumerable Values { @@ -32,6 +46,14 @@ public IEnumerable Values { if (!HasMultipleValues) throw new InvalidOperationException("Field does not have multiple values!"); + if (_value == null) + return new string[] {}; + var underlyingType = Nullable.GetUnderlyingType(_value.GetType()) ?? _value.GetType(); + if (underlyingType.IsEnum) + return Enum.GetValues(underlyingType) + .Cast() + .Where(e => (Convert.ToInt32(e) & Convert.ToInt32(_value)) != 0) + .Select(e => e.ToString()); return (_value as IEnumerable).Cast() .Select(o => new ModelFieldValue(o, _format)) .Select(v => v.Value); diff --git a/ChameleonForms.Example/Controllers/ExampleFormsController.cs b/ChameleonForms.Example/Controllers/ExampleFormsController.cs index 41639ea9..3c7998e5 100644 --- a/ChameleonForms.Example/Controllers/ExampleFormsController.cs +++ b/ChameleonForms.Example/Controllers/ExampleFormsController.cs @@ -173,6 +173,12 @@ public IFieldConfiguration ModifyConfig(IFieldConfiguration config) public SomeEnum? RequiredNullableEnum { get; set; } public SomeEnum? OptionalEnum { get; set; } + [Required, RequiredFlagsEnum] + public FlagsEnum RequiredFlagsEnum { get; set; } + [Required, RequiredFlagsEnum] + public FlagsEnum? RequiredNullableFlagsEnum { get; set; } + public FlagsEnum? OptionalFlagsEnum { get; set; } + [Required] public IEnumerable RequiredEnums { get; set; } [Required] @@ -246,6 +252,8 @@ public ViewModelExample() public string NestedField { get; set; } + public FlagsEnum FlagsEnums { get; set; } + public SomeEnum SomeEnum { get; set; } public List SomeEnums { get; set; } @@ -287,6 +295,16 @@ public class ListItem public string Name { get; set; } } + [Flags] + public enum FlagsEnum + { + One = 1, + Two = 2, + Three = 4, + Four = 8, + Five = 16 + } + public enum SomeEnum { Value1, diff --git a/ChameleonForms.Example/Global.asax.cs b/ChameleonForms.Example/Global.asax.cs index bd4b5cf1..38173651 100644 --- a/ChameleonForms.Example/Global.asax.cs +++ b/ChameleonForms.Example/Global.asax.cs @@ -1,6 +1,8 @@ using System; +using System.Linq; using System.Web.Mvc; using System.Web.Routing; +using ChameleonForms.Example.Controllers; using ChameleonForms.Example.Controllers.Filters; using ChameleonForms.ModelBinders; @@ -14,6 +16,12 @@ protected void Application_Start() HumanizedLabels.Register(); System.Web.Mvc.ModelBinders.Binders.Add(typeof(DateTime), new DateTimeModelBinder()); System.Web.Mvc.ModelBinders.Binders.Add(typeof(DateTime?), new DateTimeModelBinder()); + typeof(ExampleFormsController).Assembly.GetTypes().Where(t => t.IsEnum && t.GetCustomAttributes(typeof(FlagsAttribute), false).Any()) + .ToList().ForEach(t => + { + System.Web.Mvc.ModelBinders.Binders.Add(t, new FlagsEnumModelBinder()); + System.Web.Mvc.ModelBinders.Binders.Add(typeof(Nullable<>).MakeGenericType(t), new FlagsEnumModelBinder()); + }); GlobalFilters.Filters.Add(new FormTemplateFilter()); } } diff --git a/ChameleonForms.Example/Views/ExampleForms/Form1.cshtml b/ChameleonForms.Example/Views/ExampleForms/Form1.cshtml index 1bf0cd03..5cad27e2 100644 --- a/ChameleonForms.Example/Views/ExampleForms/Form1.cshtml +++ b/ChameleonForms.Example/Views/ExampleForms/Form1.cshtml @@ -19,6 +19,7 @@

@f.FieldElementFor(m => m.RequiredStringField).TabIndex(4)

using (var s = f.BeginSection("My Section!", InstructionalText(), new{@class = "aClass"}.ToHtmlAttributes())) { + @s.FieldFor(m => m.FlagsEnums) @s.FieldFor(m=>m.Boolean).WithoutInlineLabel() using (var ff = s.BeginFieldFor(m => m.RequiredStringField, Field.Configure().Attr("data-some-attr", "value").TabIndex(3))) { diff --git a/ChameleonForms.Example/Views/ExampleForms/ModelBindingExample.cshtml b/ChameleonForms.Example/Views/ExampleForms/ModelBindingExample.cshtml index 9dc51b61..963c959d 100644 --- a/ChameleonForms.Example/Views/ExampleForms/ModelBindingExample.cshtml +++ b/ChameleonForms.Example/Views/ExampleForms/ModelBindingExample.cshtml @@ -17,6 +17,9 @@ @Model.ModifyConfig(s.FieldFor(m => m.RequiredEnum)) @Model.ModifyConfig(s.FieldFor(m => m.RequiredNullableEnum)) @Model.ModifyConfig(s.FieldFor(m => m.OptionalEnum)) + @Model.ModifyConfig(s.FieldFor(m => m.RequiredFlagsEnum)) + @Model.ModifyConfig(s.FieldFor(m => m.RequiredNullableFlagsEnum)) + @Model.ModifyConfig(s.FieldFor(m => m.OptionalFlagsEnum)) @Model.ModifyConfig(s.FieldFor(m => m.RequiredEnums)) @Model.ModifyConfig(s.FieldFor(m => m.RequiredNullableEnums)) @Model.ModifyConfig(s.FieldFor(m => m.OptionalEnums)) diff --git a/ChameleonForms.Tests/Attributes/RequiredFlagsEnumAttributeTests.cs b/ChameleonForms.Tests/Attributes/RequiredFlagsEnumAttributeTests.cs new file mode 100644 index 00000000..08e4cf6b --- /dev/null +++ b/ChameleonForms.Tests/Attributes/RequiredFlagsEnumAttributeTests.cs @@ -0,0 +1,41 @@ +using ChameleonForms.Attributes; +using ChameleonForms.Tests.FieldGenerator; +using NUnit.Framework; + +namespace ChameleonForms.Tests.Attributes +{ + class RequiredFlagsEnumAttributeTests + { + [Test] + public void DefaultFlagsEnumValueShouldntBeValid() + { + var attr = new RequiredFlagsEnumAttribute(); + + Assert.That(attr.IsValid(default(TestFlagsEnum)), Is.False); + } + + [Test] + public void NonDefaultFlagsEnumSingleValueShouldBeValid() + { + var attr = new RequiredFlagsEnumAttribute(); + + Assert.That(attr.IsValid(TestFlagsEnum.Simplevalue), Is.True); + } + + [Test] + public void NonDefaultFlagsEnumMultipleValueShouldBeValid() + { + var attr = new RequiredFlagsEnumAttribute(); + + Assert.That(attr.IsValid(TestFlagsEnum.Simplevalue | TestFlagsEnum.ValueWithDescriptionAttribute), Is.True); + } + + [Test] + public void NullShouldntBeValid() + { + var attr = new RequiredFlagsEnumAttribute(); + + Assert.That(attr.IsValid(null), Is.False); + } + } +} diff --git a/ChameleonForms.Tests/ChameleonForms.Tests.csproj b/ChameleonForms.Tests/ChameleonForms.Tests.csproj index d2e77061..2eeb7a3d 100644 --- a/ChameleonForms.Tests/ChameleonForms.Tests.csproj +++ b/ChameleonForms.Tests/ChameleonForms.Tests.csproj @@ -129,6 +129,7 @@ + @@ -139,6 +140,7 @@ + @@ -158,6 +160,7 @@ + diff --git a/ChameleonForms.Tests/Component/Config/FieldConfigurationTests.cs b/ChameleonForms.Tests/Component/Config/FieldConfigurationTests.cs index cde9ca02..b4c0a25b 100644 --- a/ChameleonForms.Tests/Component/Config/FieldConfigurationTests.cs +++ b/ChameleonForms.Tests/Component/Config/FieldConfigurationTests.cs @@ -449,7 +449,7 @@ public void Have_label_by_default() { var fc = Field.Configure(); - Assert.That(fc.HasLabel, Is.True); + Assert.That(fc.HasLabelElement, Is.True); } [Test] diff --git a/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_child_viewmodel_enum_list_field.approved.html b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_child_viewmodel_enum_list_field.approved.html new file mode 100644 index 00000000..3b08940b --- /dev/null +++ b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_child_viewmodel_enum_list_field.approved.html @@ -0,0 +1,14 @@ +
    +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
\ No newline at end of file diff --git a/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_enum_list_with_excluded_value.approved.html b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_enum_list_with_excluded_value.approved.html new file mode 100644 index 00000000..48300ec6 --- /dev/null +++ b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_enum_list_with_excluded_value.approved.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_label_for_enum_dropdown.approved.html b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_label_for_enum_dropdown.approved.html new file mode 100644 index 00000000..1fb95093 --- /dev/null +++ b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_label_for_enum_dropdown.approved.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_label_for_enum_list.approved.html b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_label_for_enum_list.approved.html new file mode 100644 index 00000000..64ea381c --- /dev/null +++ b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_label_for_enum_list.approved.html @@ -0,0 +1 @@ +RequiredFlagsEnum \ No newline at end of file diff --git a/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_label_for_enum_list_with_overridden_label.approved.html b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_label_for_enum_list_with_overridden_label.approved.html new file mode 100644 index 00000000..f65b9144 --- /dev/null +++ b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_label_for_enum_list_with_overridden_label.approved.html @@ -0,0 +1 @@ +lol \ No newline at end of file diff --git a/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_nullable_required_enum_field.approved.html b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_nullable_required_enum_field.approved.html new file mode 100644 index 00000000..2bfb91b9 --- /dev/null +++ b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_nullable_required_enum_field.approved.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_optional_enum_field.approved.html b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_optional_enum_field.approved.html new file mode 100644 index 00000000..62f8da50 --- /dev/null +++ b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_optional_enum_field.approved.html @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_optional_enum_field_with_null_string_attribute.approved.html b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_optional_enum_field_with_null_string_attribute.approved.html new file mode 100644 index 00000000..3cd1c36b --- /dev/null +++ b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_optional_enum_field_with_null_string_attribute.approved.html @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_optional_enum_field_with_overridden_null_string_attribute.approved.html b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_optional_enum_field_with_overridden_null_string_attribute.approved.html new file mode 100644 index 00000000..805b07a6 --- /dev/null +++ b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_optional_enum_field_with_overridden_null_string_attribute.approved.html @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_required_enum_field.approved.html b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_required_enum_field.approved.html new file mode 100644 index 00000000..fe29da08 --- /dev/null +++ b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_required_enum_field.approved.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.cs b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.cs new file mode 100644 index 00000000..da276c6f --- /dev/null +++ b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.cs @@ -0,0 +1,110 @@ +using System.Web; +using ApprovalTests.Html; +using ChameleonForms.Component.Config; +using NUnit.Framework; + +namespace ChameleonForms.Tests.FieldGenerator.DefaultFieldGenerator +{ + class FlagsEnumTests : DefaultFieldGeneratorShould + { + [Test] + public void Use_correct_html_for_required_enum_field() + { + var g = Arrange(m => m.RequiredFlagsEnum, m => m.RequiredFlagsEnum = TestFlagsEnum.ValueWithDescriptionAttribute | TestFlagsEnum.Simplevalue); + + var result = g.GetFieldHtml(ExampleFieldConfiguration); + + HtmlApprovals.VerifyHtml(result.ToHtmlString()); + } + + [Test] + public void Use_correct_html_for_optional_enum_field_with_null_string_attribute() + { + var g = Arrange(m => m.OptionalFlagsEnumWithNullStringAttribute, m => m.OptionalFlagsEnumWithNullStringAttribute = null); + + var result = g.GetFieldHtml(default(IFieldConfiguration)); + + HtmlApprovals.VerifyHtml(result.ToHtmlString()); + } + + [Test] + public void Use_correct_html_for_optional_enum_field_with_overridden_null_string_attribute() + { + var g = Arrange(m => m.OptionalFlagsEnumWithNullStringAttribute, m => m.OptionalFlagsEnumWithNullStringAttribute = null); + + var result = g.GetFieldHtml(new FieldConfiguration().WithNoneAs("Overridden")); + + HtmlApprovals.VerifyHtml(result.ToHtmlString()); + } + + [Test] + public void Use_correct_html_for_optional_enum_field() + { + var g = Arrange(m => m.OptionalFlagsEnum, m => m.OptionalFlagsEnum = null); + + var result = g.GetFieldHtml(ExampleFieldConfiguration); + + HtmlApprovals.VerifyHtml(result.ToHtmlString()); + } + + [Test] + public void Use_correct_html_for_nullable_required_enum_field() + { + var g = Arrange(m => m.RequiredNullableFlagsEnum); + + var result = g.GetFieldHtml(ExampleFieldConfiguration); + + HtmlApprovals.VerifyHtml(result.ToHtmlString()); + } + + [Test] + public void Use_correct_html_for_label_for_enum_list() + { + var g = Arrange(m => m.RequiredFlagsEnum); + + var result = g.GetLabelHtml(new FieldConfiguration().AsRadioList()); + + HtmlApprovals.VerifyHtml(result.ToHtmlString()); + } + + [Test] + public void Use_correct_html_for_label_for_enum_list_with_overridden_label() + { + var g = Arrange(m => m.RequiredFlagsEnum); + + var result = g.GetLabelHtml(new FieldConfiguration().AsRadioList().Label(new HtmlString("lol"))); + + HtmlApprovals.VerifyHtml(result.ToHtmlString()); + } + + [Test] + public void Use_correct_html_for_label_for_enum_dropdown() + { + var g = Arrange(m => m.RequiredFlagsEnum); + + var result = g.GetLabelHtml(new FieldConfiguration().AsDropDown()); + + HtmlApprovals.VerifyHtml(result.ToHtmlString()); + } + + [Test] + public void Use_correct_html_for_child_viewmodel_enum_list_field() + { + var g = Arrange(m => m.Child.RequiredChildFlagsEnum); + + var result = g.GetFieldHtml(ExampleFieldConfiguration.AsRadioList()); + + HtmlApprovals.VerifyHtml(result.ToHtmlString()); + } + + [Test] + public void Use_correct_html_for_enum_list_with_excluded_value() + { + var g = Arrange(m => m.RequiredFlagsEnum); + + var result = g.GetFieldHtml(ExampleFieldConfiguration.Exclude(TestFlagsEnum.Simplevalue)); + + HtmlApprovals.VerifyHtml(result.ToHtmlString()); + } + } +} diff --git a/ChameleonForms.Tests/FieldGenerator/DefaultFieldGeneratorTests.cs b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGeneratorTests.cs index c0b6f92a..f261db75 100644 --- a/ChameleonForms.Tests/FieldGenerator/DefaultFieldGeneratorTests.cs +++ b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGeneratorTests.cs @@ -22,7 +22,16 @@ public enum TestEnum Simplevalue, [Description("Description attr text")] ValueWithDescriptionAttribute, - ValueWithMultpipleWordsAndNoDescriptionAttribute, + ValueWithMultpipleWordsAndNoDescriptionAttribute + } + + [Flags] + public enum TestFlagsEnum + { + Simplevalue = 1, + [Description("Description attr text")] + ValueWithDescriptionAttribute = 2, + ValueWithMultpipleWordsAndNoDescriptionAttribute = 4 } public class TestFieldViewModel @@ -56,14 +65,21 @@ public TestFieldViewModel() public DateTime DateTimeWithGFormat { get; set; } public TestEnum RequiredEnum { get; set; } + [Required, RequiredFlagsEnum] + public TestFlagsEnum RequiredFlagsEnum { get; set; } [Required] public TestEnum? RequiredNullableEnum { get; set; } + [Required, RequiredFlagsEnum] + public TestFlagsEnum? RequiredNullableFlagsEnum { get; set; } [DisplayFormat(NullDisplayText = "Nothing to see here")] public TestEnum? OptionalEnumWithNullStringAttribute { get; set; } + [DisplayFormat(NullDisplayText = "Nothing to see here")] + public TestFlagsEnum? OptionalFlagsEnumWithNullStringAttribute { get; set; } public TestEnum? OptionalEnum { get; set; } + public TestFlagsEnum? OptionalFlagsEnum { get; set; } [Required] public IEnumerable RequiredEnumList { get; set; } @@ -145,6 +161,8 @@ public class StringListItem public class ChildViewModel { public TestEnum RequiredChildEnum { get; set; } + [Required, RequiredFlagsEnum] + public TestFlagsEnum RequiredChildFlagsEnum { get; set; } } [TestFixture] @@ -180,55 +198,58 @@ protected DefaultFieldGenerator Arrange(Expression(H, property, new DefaultFormTemplate()); } - [Test] - public void Not_throw_exception_getting_model_when_view_model_is_null() + class DefaultFieldGeneratorTests : DefaultFieldGeneratorShould { - var generator = Arrange(m => m.Decimal); - H.ViewData.Model = null; - H.ViewData.ModelMetadata.Model = null; - - generator.GetModel(); - } + [Test] + public void Not_throw_exception_getting_model_when_view_model_is_null() + { + var generator = Arrange(m => m.Decimal); + H.ViewData.Model = null; + H.ViewData.ModelMetadata.Model = null; - [Test] - public void Not_throw_exception_getting_value_when_view_model_is_null() - { - var generator = Arrange(m => m.Decimal); - H.ViewData.Model = null; - H.ViewData.ModelMetadata.Model = null; + generator.GetModel(); + } - generator.GetValue(); - } - - [Test] - public void Return_property_name() - { - var generator = Arrange(m => m.DecimalWithFormatStringAttribute); + [Test] + public void Not_throw_exception_getting_value_when_view_model_is_null() + { + var generator = Arrange(m => m.Decimal); + H.ViewData.Model = null; + H.ViewData.ModelMetadata.Model = null; - var name = generator.GetFieldId(); + generator.GetValue(); + } - Assert.That(name, Is.EqualTo("DecimalWithFormatStringAttribute")); - } + [Test] + public void Return_property_name() + { + var generator = Arrange(m => m.DecimalWithFormatStringAttribute); - [Test] - public void Set_field_configuration_if_readonly_attribute_applied() - { - var generator = Arrange(m => m.ReadonlyInt); - - var configuration = generator.PrepareFieldConfiguration(ExampleFieldConfiguration, FieldParent.Section); + var name = generator.GetFieldId(); - Assert.That(configuration.HtmlAttributes["readonly"], Is.EqualTo("readonly")); - } + Assert.That(name, Is.EqualTo("DecimalWithFormatStringAttribute")); + } - [Test] - public void GetLabelHtml_should_return_display_attribute_if_WithoutLabelElement_used_and_DisplayAttribute_present() - { - var generator = Arrange(x => x.StringWithDisplayAttribute); - var fieldConfig = new FieldConfiguration(); - fieldConfig.WithoutLabelElement(); - var config = generator.PrepareFieldConfiguration(fieldConfig, FieldParent.Section); - var actual = generator.GetLabelHtml(config).ToString(); - Assert.That(actual, Is.EqualTo("Use this display name")); + [Test] + public void Set_field_configuration_if_readonly_attribute_applied() + { + var generator = Arrange(m => m.ReadonlyInt); + + var configuration = generator.PrepareFieldConfiguration(ExampleFieldConfiguration, FieldParent.Section); + + Assert.That(configuration.HtmlAttributes["readonly"], Is.EqualTo("readonly")); + } + + [Test] + public void GetLabelHtml_should_return_display_attribute_if_WithoutLabelElement_used_and_DisplayAttribute_present() + { + var generator = Arrange(x => x.StringWithDisplayAttribute); + var fieldConfig = new FieldConfiguration(); + fieldConfig.WithoutLabelElement(); + var config = generator.PrepareFieldConfiguration(fieldConfig, FieldParent.Section); + var actual = generator.GetLabelHtml(config).ToString(); + Assert.That(actual, Is.EqualTo("Use this display name")); + } } } } \ No newline at end of file diff --git a/ChameleonForms.Tests/ModelBinders/FlagsEnumModelBinderShould.cs b/ChameleonForms.Tests/ModelBinders/FlagsEnumModelBinderShould.cs new file mode 100644 index 00000000..37cdbb13 --- /dev/null +++ b/ChameleonForms.Tests/ModelBinders/FlagsEnumModelBinderShould.cs @@ -0,0 +1,115 @@ +using System.Web.Mvc; +using ChameleonForms.ModelBinders; +using ChameleonForms.Tests.Helpers; +using NUnit.Framework; +using System.Globalization; +using ChameleonForms.Tests.FieldGenerator; + +namespace ChameleonForms.Tests.ModelBinders +{ + [TestFixture(TypeArgs = new[]{typeof(TestFlagsEnum)})] + [TestFixture(TypeArgs = new[]{typeof(TestFlagsEnum?)})] + class FlagsEnumModelBinderShould + { + #region Setup + private ControllerContext _context; + private FormCollection _formCollection; + + private const string PropertyName = "Property"; + private const string DisplayName = "Display Name"; + + [SetUp] + public void Setup() + { + var c = AutoSubstituteContainer.Create(); + _context = c.Resolve(); + _formCollection = new FormCollection(); + + System.Threading.Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; + } + + private ModelBindingContext ArrangeBindingContext() + { + var modelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(T)); + modelMetadata.DisplayName = DisplayName; + return new ModelBindingContext + { + ModelName = PropertyName, + ValueProvider = _formCollection.ToValueProvider(), + ModelMetadata = modelMetadata, + ModelState = new ModelStateDictionary(), + }; + } + + private T BindModel(ModelBindingContext bindingContext) + { + return (T) (new FlagsEnumModelBinder().BindModel(_context, bindingContext) ?? default(T)); + } + + private static void AssertModelError(ModelBindingContext context, string error) + { + Assert.That(context.ModelState.ContainsKey(PropertyName), PropertyName + " not present in model state"); + Assert.That(context.ModelState[PropertyName].Errors.Count, Is.EqualTo(1), "Expecting an error against " + PropertyName); + Assert.That(context.ModelState[PropertyName].Errors[0].ErrorMessage, Is.EqualTo(error), "Expecting different error message for model state against " + PropertyName); + } + #endregion + + [Test] + public void Use_default_model_binder_when_there_is_no_value([Values("", null)] string value) + { + _formCollection[PropertyName] = value; + var context = ArrangeBindingContext(); + + var model = BindModel(context); + + Assert.That(model, Is.EqualTo(default(T))); + Assert.That(context.ModelState.IsValid); + } + + [Test] + public void Use_default_value_when_there_is_an_invalid_value() + { + _formCollection[PropertyName] = "invalid"; + var context = ArrangeBindingContext(); + + var model = BindModel(context); + + Assert.That(model, Is.EqualTo(default(T))); + } + + [Test] + public void Add_modelstate_error_when_there_is_an_invalid_value() + { + _formCollection[PropertyName] = "invalid"; + var context = ArrangeBindingContext(); + + BindModel(context); + + AssertModelError(context, string.Format("The value 'invalid' is not valid for {0}.", DisplayName)); + } + + [Test] + public void Return_and_bind_value_if_single_value_ok() + { + _formCollection[PropertyName] = TestFlagsEnum.Simplevalue.ToString(); + var context = ArrangeBindingContext(); + + var model = BindModel(context); + + Assert.That(model, Is.EqualTo(TestFlagsEnum.Simplevalue)); + Assert.That(context.Model, Is.EqualTo(TestFlagsEnum.Simplevalue)); + } + + [Test] + public void Return_and_bind_value_if_multiple_value_ok() + { + _formCollection[PropertyName] = TestFlagsEnum.Simplevalue + "," + TestFlagsEnum.ValueWithDescriptionAttribute; + var context = ArrangeBindingContext(); + + var model = BindModel(context); + + Assert.That(model, Is.EqualTo(TestFlagsEnum.Simplevalue | TestFlagsEnum.ValueWithDescriptionAttribute)); + Assert.That(context.Model, Is.EqualTo(TestFlagsEnum.Simplevalue | TestFlagsEnum.ValueWithDescriptionAttribute)); + } + } +} diff --git a/ChameleonForms.sln.DotSettings b/ChameleonForms.sln.DotSettings index 06c35906..09b875ac 100644 --- a/ChameleonForms.sln.DotSettings +++ b/ChameleonForms.sln.DotSettings @@ -1,4 +1,5 @@  + DO_NOT_SHOW DO_NOT_SHOW <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb_aaBb" /></Policy> <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Public" Description=""><ElementKinds><Kind Name="TEST_MEMBER" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb_aaBb" /></Policy></Policy> diff --git a/ChameleonForms/Attributes/RequiredFlagsEnumAttribute.cs b/ChameleonForms/Attributes/RequiredFlagsEnumAttribute.cs new file mode 100644 index 00000000..18ce7eb5 --- /dev/null +++ b/ChameleonForms/Attributes/RequiredFlagsEnumAttribute.cs @@ -0,0 +1,20 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace ChameleonForms.Attributes +{ + /// + /// Marks a Flags enum property as required. + /// + public class RequiredFlagsEnumAttribute : RequiredAttribute + { + /// + public override bool IsValid(object value) + { + if (Convert.ToInt64(value) == 0L) + return false; + + return base.IsValid(value); + } + } +} diff --git a/ChameleonForms/ChameleonForms.csproj b/ChameleonForms/ChameleonForms.csproj index 2f0ed2a5..ad0d4c61 100644 --- a/ChameleonForms/ChameleonForms.csproj +++ b/ChameleonForms/ChameleonForms.csproj @@ -76,6 +76,7 @@
+ @@ -90,6 +91,7 @@ + @@ -157,4 +159,4 @@ --> - + \ No newline at end of file diff --git a/ChameleonForms/FieldGenerators/Handlers/BooleanHandler.cs b/ChameleonForms/FieldGenerators/Handlers/BooleanHandler.cs index e9fa4062..8b4d4156 100644 --- a/ChameleonForms/FieldGenerators/Handlers/BooleanHandler.cs +++ b/ChameleonForms/FieldGenerators/Handlers/BooleanHandler.cs @@ -25,7 +25,8 @@ public BooleanHandler(IFieldGenerator fieldGenerator) /// public override bool CanHandle() { - return GetUnderlyingType(FieldGenerator) == typeof(bool); + return GetUnderlyingType(FieldGenerator) == typeof(bool) + && !HasEnumerableValues(FieldGenerator); } /// diff --git a/ChameleonForms/FieldGenerators/Handlers/FieldGeneratorHandler.cs b/ChameleonForms/FieldGenerators/Handlers/FieldGeneratorHandler.cs index e2f27108..93b6890c 100644 --- a/ChameleonForms/FieldGenerators/Handlers/FieldGeneratorHandler.cs +++ b/ChameleonForms/FieldGenerators/Handlers/FieldGeneratorHandler.cs @@ -16,7 +16,9 @@ namespace ChameleonForms.FieldGenerators.Handlers /// /// The type of the model the form is being output for /// The type of the property in the model that the specific field is being output for + // ReSharper disable UnusedTypeParameter public interface IFieldGeneratorHandler + // ReSharper enable UnusedTypeParameter { /// /// Whether or not the current field can be output using this field generator handler. @@ -89,50 +91,104 @@ public virtual void PrepareFieldConfiguration(IFieldConfiguration fieldConfigura /// public abstract FieldDisplayType GetDisplayType(IReadonlyFieldConfiguration fieldConfiguration); - /// + /// + /// Whether or not the field represented by the field generator allows the user to enter multiple values. + /// + /// The field generator wrapping the field + /// Whether or not the user can enter multiple values protected static bool HasMultipleValues(IFieldGenerator fieldGenerator) { - return fieldGenerator.Metadata.ModelType.IsGenericType - && typeof(IEnumerable).IsAssignableFrom(fieldGenerator.Metadata.ModelType); + return HasMultipleEnumValues(fieldGenerator) || HasEnumerableValues(fieldGenerator); } - /// - protected static IEnumerable GetValues(IFieldGenerator fieldGenerator) + /// + /// Whether or not the field represented by the field generator is an enum that can represent multiple values. + /// i.e. whether or not the field is a flags enum. + /// + /// The field generator wrapping the field + /// Whether or not the field is a flags enum + protected static bool HasMultipleEnumValues(IFieldGenerator fieldGenerator) + { + return !HasEnumerableValues(fieldGenerator) + && GetUnderlyingType(fieldGenerator).IsEnum + && GetUnderlyingType(fieldGenerator).GetCustomAttributes(typeof(FlagsAttribute), false).Any(); + } + + /// + /// Whether or not the field represented by the field generator is an enumerable list that allows multiple values. + /// i.e. whether or not the field is an + /// + /// The field generator wrapping the field + /// Whether or not the field is an + protected static bool HasEnumerableValues(IFieldGenerator fieldGenerator) + { + return typeof(IEnumerable).IsAssignableFrom(fieldGenerator.Metadata.ModelType) + && fieldGenerator.Metadata.ModelType.IsGenericType; + } + + /// + /// Returns the enumerated values of a field that is an . + /// + /// The field generator wrapping the field + /// The enumerated values of the field + protected static IEnumerable GetEnumerableValues(IFieldGenerator fieldGenerator) { return (((IEnumerable)fieldGenerator.GetValue()) ?? new object[]{}).Cast(); } - /// + /// + /// Whether or not the given value is present for the field represented by the field generator. + /// + /// The value to check is selected + /// The field generator wrapping the field + /// Whether or not the value is selected protected static bool IsSelected(object value, IFieldGenerator fieldGenerator) { - if (HasMultipleValues(fieldGenerator)) - return GetValues(fieldGenerator).Contains(value); + if (HasEnumerableValues(fieldGenerator)) + return GetEnumerableValues(fieldGenerator).Contains(value); var val = fieldGenerator.GetValue(); - if (val != null) - return val.Equals(value); + if (val == null) + return value == null; + + if (HasMultipleEnumValues(fieldGenerator)) + return (Convert.ToInt32(fieldGenerator.GetValue()) & Convert.ToInt32(value)) != 0; - return value == null; + return val.Equals(value); } - /// + /// + /// Returns the underlying type of the field - unwrapping and and IEnumerable<Nullable<T>>. + /// + /// The field generator wrapping the field + /// The underlying type of the field protected static Type GetUnderlyingType(IFieldGenerator fieldGenerator) { var type = fieldGenerator.Metadata.ModelType; - if (HasMultipleValues(fieldGenerator)) + if (HasEnumerableValues(fieldGenerator)) type = type.GetGenericArguments()[0]; return Nullable.GetUnderlyingType(type) ?? type; } - /// + /// + /// Whether or not the field involves collection of numeric values. + /// + /// The field generator wrapping the field + /// Whether or not the field involves collection of numeric values protected static bool IsNumeric(IFieldGenerator fieldGenerator) { return FieldGeneratorHandler.NumericTypes.Contains(GetUnderlyingType(fieldGenerator)); } - /// + /// + /// Returns HTML for an <input> HTML element. + /// + /// The type of input to produce + /// The field generator wrapping the field + /// The field configuration to use for attributes and format string + /// The HTML of the input element protected static IHtmlString GetInputHtml(TextInputType inputType, IFieldGenerator fieldGenerator, IReadonlyFieldConfiguration fieldConfiguration) { if (inputType == TextInputType.Password) @@ -140,7 +196,7 @@ protected static IHtmlString GetInputHtml(TextInputType inputType, IFieldGenerat var attrs = new HtmlAttributes(fieldConfiguration.HtmlAttributes); if (!attrs.Attributes.ContainsKey("type")) - attrs.Attr(type => inputType.ToString().ToLower()); + attrs = attrs.Attr(type => inputType.ToString().ToLower()); return !string.IsNullOrEmpty(fieldConfiguration.FormatString) ? fieldGenerator.HtmlHelper.TextBoxFor(fieldGenerator.FieldProperty, fieldConfiguration.FormatString, attrs.ToDictionary()) : fieldGenerator.HtmlHelper.TextBoxFor(fieldGenerator.FieldProperty, attrs.ToDictionary()); @@ -176,7 +232,14 @@ private static bool HasEmptySelectListItem(IFieldGenerator fieldGener return false; } - /// + /// + /// Returns the HTML of a <select> list element. + /// Automatically adds an empty item where appropriate. + /// + /// The list of items to choose from in the select list + /// The field generator wrapping the field + /// The field configuration to use for attributes and empty item configuration + /// protected static IHtmlString GetSelectListHtml(IEnumerable selectList, IFieldGenerator fieldGenerator, IReadonlyFieldConfiguration fieldConfiguration) { if (HasEmptySelectListItem(fieldGenerator, fieldConfiguration)) @@ -189,7 +252,14 @@ protected static IHtmlString GetSelectListHtml(IEnumerable selec return fieldGenerator.Template.RadioOrCheckboxList(list, isCheckbox: HasMultipleValues(fieldGenerator)); case FieldDisplayType.DropDown: case FieldDisplayType.Default: - return HasMultipleValues(fieldGenerator) + if (HasMultipleEnumValues(fieldGenerator)) + { + var attrs = new HtmlAttributes(fieldConfiguration.HtmlAttributes); + AdjustHtmlForModelState(attrs, fieldGenerator); + return HtmlCreator.BuildSelect(GetFieldName(fieldGenerator), selectList, multiple: true, htmlAttributes: attrs); + } + + return HasEnumerableValues(fieldGenerator) ? fieldGenerator.HtmlHelper.ListBoxFor( fieldGenerator.FieldProperty, selectList, fieldConfiguration.HtmlAttributes) @@ -239,8 +309,8 @@ private static IEnumerable SelectListToRadioList(IEnumerable + /// + /// The value to use for the name of a field (e.g. for the name attribute or looking up model state). + /// + /// The field generator wrapping the field + /// The name of the field protected static string GetFieldName(IFieldGenerator fieldGenerator) { var name = ExpressionHelper.GetExpressionText(fieldGenerator.FieldProperty); return fieldGenerator.HtmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name); } - /// + /// + /// Adjust the HTML attributes of a field based on the state of the model for that field. + /// e.g. add validation attributes and error attributes. + /// + /// The attributes to modify + /// The field generator wrapping the field protected static void AdjustHtmlForModelState(HtmlAttributes attrs, IFieldGenerator fieldGenerator) { var name = ExpressionHelper.GetExpressionText(fieldGenerator.FieldProperty); @@ -271,12 +350,12 @@ protected static void AdjustHtmlForModelState(HtmlAttributes attrs, IFieldGenera { if (modelState.Errors.Count > 0) { - attrs.AddClass(HtmlHelper.ValidationInputCssClassName); + attrs = attrs.AddClass(HtmlHelper.ValidationInputCssClassName); } } + // ReSharper disable once MustUseReturnValue attrs.Attrs(fieldGenerator.HtmlHelper.GetUnobtrusiveValidationAttributes(name, ModelMetadata.FromLambdaExpression(fieldGenerator.FieldProperty, fieldGenerator.HtmlHelper.ViewData))); } } - } diff --git a/ChameleonForms/ModelBinders/FlagsEnumModelBinder.cs b/ChameleonForms/ModelBinders/FlagsEnumModelBinder.cs new file mode 100644 index 00000000..e916e300 --- /dev/null +++ b/ChameleonForms/ModelBinders/FlagsEnumModelBinder.cs @@ -0,0 +1,54 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Web.Mvc; + +namespace ChameleonForms.ModelBinders +{ + /// + /// Binds a flags enum in a model. + /// + public class FlagsEnumModelBinder : DefaultModelBinder + { + /// + public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + var underlyingType = Nullable.GetUnderlyingType(bindingContext.ModelType) ?? bindingContext.ModelType; + var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); + var submittedValue = value == null ? null : value.AttemptedValue; + + if ( + !underlyingType.IsEnum + || + !underlyingType.GetCustomAttributes(typeof(FlagsAttribute), false).Any() + || + (string.IsNullOrEmpty(submittedValue)) + ) + return base.BindModel(controllerContext, bindingContext); + + var enumValueAsLong = 0L; + var enumValues = submittedValue.Split(','); + var error = false; + + foreach (var v in enumValues) + { + if (Enum.IsDefined(underlyingType, v)) + { + var valueAsEnum = Enum.Parse(underlyingType, v, true); + enumValueAsLong |= Convert.ToInt64(valueAsEnum); + } + else + { + error = true; + bindingContext.ModelState.AddModelError(bindingContext.ModelName, string.Format("The value '{0}' is not valid for {1}.", v, bindingContext.ModelMetadata.DisplayName ?? bindingContext.ModelMetadata.PropertyName)); + } + } + + if (error) + return Activator.CreateInstance(bindingContext.ModelType); + + bindingContext.ModelMetadata.Model = Enum.Parse(underlyingType, enumValueAsLong.ToString()); + return bindingContext.ModelMetadata.Model; + } + } +} diff --git a/ChameleonForms/Templates/HtmlCreator.cs b/ChameleonForms/Templates/HtmlCreator.cs index 4ed5ec89..1ab92b7f 100644 --- a/ChameleonForms/Templates/HtmlCreator.cs +++ b/ChameleonForms/Templates/HtmlCreator.cs @@ -1,4 +1,5 @@ -using System.Web; +using System.Collections.Generic; +using System.Web; using System.Web.Mvc; using ChameleonForms.Enums; using Humanizer; @@ -97,6 +98,40 @@ public static IHtmlString BuildSingleCheckbox(string name, bool isChecked, HtmlA return new HtmlString(t.ToString(TagRenderMode.SelfClosing)); } + /// + /// Creates the HTML for a select. + /// + /// The name/id of the select + /// The items for the select list + /// Whether or not multiple items can be selected + /// Any HTML attributes that should be applied to the select + /// The HTML for the select + public static IHtmlString BuildSelect(string name, IEnumerable selectListItems, bool multiple, HtmlAttributes htmlAttributes) + { + var t = new TagBuilder("select"); + if (name != null) + { + t.Attributes.Add("name", name); + t.GenerateId(name); + } + if (htmlAttributes != null) + t.MergeAttributes(htmlAttributes.Attributes, true); + if (multiple) + t.Attributes.Add("multiple", "multiple"); + + foreach (var item in selectListItems) + { + var option = new TagBuilder("option"); + if (item.Selected) + option.Attributes.Add("selected", "selected"); + option.Attributes.Add("value", item.Value); + option.SetInnerText(item.Text); + t.InnerHtml += option.ToString(); + } + + return new HtmlString(t.ToString()); + } + /// /// Creates the HTML for an input. /// From 657771a1f4c56a5f47a912910ceecc01d8ccb014 Mon Sep 17 00:00:00 2001 From: Rob Moore Date: Sat, 16 Jul 2016 23:45:16 +0800 Subject: [PATCH 3/6] Ensuring long-backed enums won't fail --- .../FieldGenerators/Handlers/FieldGeneratorHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChameleonForms/FieldGenerators/Handlers/FieldGeneratorHandler.cs b/ChameleonForms/FieldGenerators/Handlers/FieldGeneratorHandler.cs index 93b6890c..d87fd09d 100644 --- a/ChameleonForms/FieldGenerators/Handlers/FieldGeneratorHandler.cs +++ b/ChameleonForms/FieldGenerators/Handlers/FieldGeneratorHandler.cs @@ -152,7 +152,7 @@ protected static bool IsSelected(object value, IFieldGenerator fieldG return value == null; if (HasMultipleEnumValues(fieldGenerator)) - return (Convert.ToInt32(fieldGenerator.GetValue()) & Convert.ToInt32(value)) != 0; + return (Convert.ToInt64(fieldGenerator.GetValue()) & Convert.ToInt64(value)) != 0; return val.Equals(value); } From 7ad73ee25b1fac4ef45cdcdb0a46bc986ff97661 Mon Sep 17 00:00:00 2001 From: Rob Moore Date: Sun, 17 Jul 2016 18:35:04 +0800 Subject: [PATCH 4/6] Added flags enum documentation --- ChameleonForms.sln | 5 +- docs/enum.md | 8 +- docs/flags-enum.md | 141 +++++++++++++++++++++++++++++ docs/index.md | 203 +++++++++++++++++++++--------------------- docs/multiple-enum.md | 4 +- 5 files changed, 255 insertions(+), 106 deletions(-) create mode 100644 docs/flags-enum.md diff --git a/ChameleonForms.sln b/ChameleonForms.sln index 0f2b3b51..11e2e997 100644 --- a/ChameleonForms.sln +++ b/ChameleonForms.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.31101.0 +# Visual Studio 14 +VisualStudioVersion = 14.0.25123.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChameleonForms.Example", "ChameleonForms.Example\ChameleonForms.Example.csproj", "{A51A9FA4-12D6-4980-BEF9-D1DED004F16C}" EndProject @@ -59,6 +59,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{F6A47230-C docs\field-validation-html.md = docs\field-validation-html.md docs\field.md = docs\field.md docs\file-upload.md = docs\file-upload.md + docs\flags-enum.md = docs\flags-enum.md docs\form-templates.md = docs\form-templates.md docs\form.png = docs\form.png docs\getting-started.md = docs\getting-started.md diff --git a/docs/enum.md b/docs/enum.md index bd16e084..1baa9acd 100644 --- a/docs/enum.md +++ b/docs/enum.md @@ -6,10 +6,14 @@ If you want the user to specify a value from an enum you can use that enum type ```csharp public enum MyEnum { ... } ... -public MyEnum EnumField { get; set; } -public MyEnum? NullableEnumField { get; set; } +public MyEnum EnumField { get; set; } // automatically required since it's non-nullable +[Required] +public MyEnum? RequiredNullableEnumField { get; set; } // Required, but can start off as an empty value +public MyEnum? NullableEnumField { get; set; } // Not required ``` +If you want the user to select multiple enum values you can either use a [flags enum](flags-enum.md) or a [list of enums](multiple-enum.md). + Default HTML ------------ diff --git a/docs/flags-enum.md b/docs/flags-enum.md new file mode 100644 index 00000000..8b955545 --- /dev/null +++ b/docs/flags-enum.md @@ -0,0 +1,141 @@ +Flags Enum Fields +================= + +If you want the user to specify multiple values from an enum you can either use a [non-flags enum against any property with a type convertible to `IEnumerable<%enumType%>`](multiple-enum.md) or use a flags enum, e.g.: + +```csharp +[Flags] +public enum MyFlagsEnum +{ + ValueOne = 1 << 0, + ValueTwo = 1 << 1, + ValueThree = 1 << 2, + ... +} +... +[Required, RequiredFlagsEnum] +public MyFlagsEnum RequiredFlagsEnumWithZeroAsUnselectedValue { get; set; } + +[Required, RequiredFlagsEnum] +public MyFlagsEnum? RequiredFlagsEnumWithNullAsUnselectedValue { get; set; } + +public MyFlagsEnum? NonRequiredFlagsEnumAndNullAsUnselectedValue { get; set; } +``` + +Flags enums have a few rough edges on them if you aren't careful so it's a good idea to read the [guidance](https://msdn.microsoft.com/en-us/library/ms229062(v=vs.100).aspx) [for](https://msdn.microsoft.com/en-us/library/system.flagsattribute.aspx) how to use them. In particular, make sure that none of your values have a value of 0 and you explicitly assign integer values to all enum values in multiples of 2. + +If you want the user to specify a single value from an enum then you can [use the enum type directly](enum.md). + +Required validation +------------------- + +ASP.NET MVC's default validation doesn't pick up `0` for a flags enum as the field not being specified, thus you need to alter the validation for requires flags enums. ChameleonForms provides the `[RequiredFlagsEnum]` attribute to overcome that problem. You need to apply it alongside the usual `[Required]` attribute otherwise the client-side validation won't function as normal. This might look like: + +```csharp +[Required, RequiredFlagsEnum] +public MyFlagsEnum FlagsEnumField { get; set; } +``` + +Model binding +------------- + +The default MVC model binder does **not** correctly bind flags enum values. ChameleonForms provides the `FlagsEnumModelBinder` to assist with that. + +When you install ChameleonForms it should automatically register this model binder for all of the flags enum types registered in your MVC project within the `RegisterChameleonFormsComponents.cs` file. If you are upgrading from a pre version 3.0 version of ChameleonForms then the registration may not automatically add itself. Also, if you have view models / flags enums defined outside of your MVC project then the default registration won't work. This is the registration code we add (you may need to alter the assembly being scanned or alternatively explicitly register the model binder for the flags enums in question): + +```csharp + GetType().Assembly.GetTypes().Where(t => t.IsEnum && t.GetCustomAttributes(typeof(FlagsAttribute), false).Any()) + .ToList().ForEach(t => + { + System.Web.Mvc.ModelBinders.Binders.Add(t, new FlagsEnumModelBinder()); + System.Web.Mvc.ModelBinders.Binders.Add(typeof(Nullable<>).MakeGenericType(t), new FlagsEnumModelBinder()); + }); +``` + +If registering a specifc flags enum type you can simply use: + +```csharp + System.Web.Mvc.ModelBinders.Binders.Add(typeof(MyFlagsEnum), new FlagsEnumModelBinder()); + System.Web.Mvc.ModelBinders.Binders.Add(typeof(MyFlagsEnum?), new FlagsEnumModelBinder()); +``` + +Default HTML +------------ + +### Required nullable or non-nullable enum (multi-select drop-down with no empty option) + +```html + +``` + +### Non-Required nullable enum (multi-select drop-down with empty option) + +```html + +``` + +### Explanation and example + +Please see the explanation an example on the [Enum Field](enum#explanation-and-example) page. + +Configurability +--------------- + +### Display as list of checkboxes + +You can force a list of enums field to display as a list of checkboxes rather than a multi-select drop-down using the `AsCheckboxList` method on the Field Configuration, e.g.: + +```csharp +@s.FieldFor(m => m.FlagsEnum).AsCheckboxList() +``` + +This will change the default HTML for a both Required and non-Required list of enums (both nullable and non-nullable) fields as shown above to: + +```html +
    +%foreach enum value x with increment i % +
  • +%endforeach% +
+``` + +### Change the text description of none + +When you display a non-Required list of enums field as a drop-down you can change the text that is used to display the `none` value to the user. By default the text used is `None`. To change the text simply use the `WithNoneAs` method, e.g.: + +```csharp +@s.FieldFor(m => m.NonRequiredNullableFlagsEnum).WithNoneAs("No value") +``` + +This will change the default HTML for the non-Required drop-down list of enum field as shown above to: + +```html + +``` + +### Hide empty item +If you have a non-Required list of enums field then it will show the empty item and this item will be selected by default if no values are selected. If for some reason you want a non-Required list of enums field, but you would also like to hide the empty item you can do so with the `HideEmptyItem` method in the Field Configuration, e.g.: + +```csharp +@s.FieldFor(m => m.NullableEnumListField).HideEmptyItem() +``` + +This will change the default HTML for the non-Required drop-down list of enum field as shown above to: + +```html + +``` \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index db8c5762..1a3c63ed 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,102 +1,103 @@ -# ChameleonForms Documentation - -## Overview -ChameleonForms takes away the pain and repetition of building forms with ASP.NET MVC by following a philosophy of: - -* **Model-driven** defaults (e.g. enum is drop-down, `[DataType(DataType.Password)]` is password textbox) -* **DRY** up your forms - your forms will be quicker to write and easier to maintain and you won't get stuck writing the same form boilerplate markup form after form after form -* **Consistent** - consistency of the API and form structure within your forms and consistency across all forms in your site via templating -* **Declarative** syntax - specify how the form is structured rather than worrying about the boilerplate HTML markup of the form; this has the same beneficial effect as separating HTML markup and CSS -* **Beautiful, terse, fluent APIs** - it's a pleasure to read and write the code -* **Extensible and flexible** core - you can extend or completely change anything you want at any layer of ChameleonForms and you can drop out to plain HTML at any point in your form for those moments where pre-prepared field types and templates just don't cut it - -Twitter Bootstrap 3 -------------------- - -The ASP.NET MVC application templates now come powered by Bootstrap by default. Have you noticed the gross boilerplate HTML that is repeated again and again in every view though?! Ugh! - -ChameleonForms has got you covered; it has built-in support for building forms using **[Twitter Bootstrap 3](bootstrap-template.md)**! - -![Example of the code and display of a Chameleon-powered Bootstrap form](bootstrap-example-banner.png) - -Note: The example above shows a vertical layout, but the ASP.NET MVC templates use the horizontal layout. See the [Twitter Bootstrap 3 template documentation page](bootstrap-template.md#horizontal-and-inline-forms) to see more about the horizontal form. - -What does a ChameleonForms form look like? ------------------------------------------- -So what does a ChameleonForms form look like? Here is a (very) basic example: - -```csharp -@using (var f = Html.BeginChameleonForm()) { - using (var s = f.BeginSection("Signup for an account")) { - @s.FieldFor(m => m.FirstName) - @s.FieldFor(m => m.LastName) - @s.FieldFor(m => m.Mobile).Placeholder("04XX XXX XXX") - @s.FieldFor(m => m.LicenseAgreement).InlineLabel("I agree to the terms and conditions") - } - using (var n = f.BeginNavigation()) { - @n.Submit("Create") - } -} -``` - -We expect that you know how to use ASP.NET MVC's form generation, model-binding and validation support to be able to effectively use and understand this library. If you need a hand getting started with that knowledge then [see below](index.md#aspnet-mvc-posts). - -## Philosophy -* [Why is ChameleonForms needed](why.md) - A rant about building forms and why ChameleonForms removes a lot of the pain - -## Basic usage -* [Getting started](getting-started.md) - What does ChameleonForms do for me? How are ChameleonForms forms structured? What terminology is used in ChameleonForms? Namespaces in `Views\web.config`. -* [Comparison](comparison.md) - See an example of a ChameleonForms form versus a traditional MVC form (HTML Helpers and Editor Templates) -* [Changing to Twitter Bootstrap 3 template](bootstrap-template.md) - Changing from the default template to the Twitter Bootstrap template -* [Automatically sentence case form labels](auto-sentence-case.md) - How to automatically sentence case your form labels without having to annotate them with `[DisplayName]` by adding a single line to `global.asax` -* [Field Configuration](field-configuration.md) - An overview of the common options available to configure a form field via the `IFieldConfiguration` interface -* [HTML Attributes](html-attributes.md) - An overview of how to define HTML attributes using the `HtmlAttributes` class +# ChameleonForms Documentation + +## Overview +ChameleonForms takes away the pain and repetition of building forms with ASP.NET MVC by following a philosophy of: + +* **Model-driven** defaults (e.g. enum is drop-down, `[DataType(DataType.Password)]` is password textbox) +* **DRY** up your forms - your forms will be quicker to write and easier to maintain and you won't get stuck writing the same form boilerplate markup form after form after form +* **Consistent** - consistency of the API and form structure within your forms and consistency across all forms in your site via templating +* **Declarative** syntax - specify how the form is structured rather than worrying about the boilerplate HTML markup of the form; this has the same beneficial effect as separating HTML markup and CSS +* **Beautiful, terse, fluent APIs** - it's a pleasure to read and write the code +* **Extensible and flexible** core - you can extend or completely change anything you want at any layer of ChameleonForms and you can drop out to plain HTML at any point in your form for those moments where pre-prepared field types and templates just don't cut it + +Twitter Bootstrap 3 +------------------- + +The ASP.NET MVC application templates now come powered by Bootstrap by default. Have you noticed the gross boilerplate HTML that is repeated again and again in every view though?! Ugh! + +ChameleonForms has got you covered; it has built-in support for building forms using **[Twitter Bootstrap 3](bootstrap-template.md)**! + +![Example of the code and display of a Chameleon-powered Bootstrap form](bootstrap-example-banner.png) + +Note: The example above shows a vertical layout, but the ASP.NET MVC templates use the horizontal layout. See the [Twitter Bootstrap 3 template documentation page](bootstrap-template.md#horizontal-and-inline-forms) to see more about the horizontal form. + +What does a ChameleonForms form look like? +------------------------------------------ +So what does a ChameleonForms form look like? Here is a (very) basic example: + +```csharp +@using (var f = Html.BeginChameleonForm()) { + using (var s = f.BeginSection("Signup for an account")) { + @s.FieldFor(m => m.FirstName) + @s.FieldFor(m => m.LastName) + @s.FieldFor(m => m.Mobile).Placeholder("04XX XXX XXX") + @s.FieldFor(m => m.LicenseAgreement).InlineLabel("I agree to the terms and conditions") + } + using (var n = f.BeginNavigation()) { + @n.Submit("Create") + } +} +``` + +We expect that you know how to use ASP.NET MVC's form generation, model-binding and validation support to be able to effectively use and understand this library. If you need a hand getting started with that knowledge then [see below](index.md#aspnet-mvc-posts). + +## Philosophy +* [Why is ChameleonForms needed](why.md) - A rant about building forms and why ChameleonForms removes a lot of the pain + +## Basic usage +* [Getting started](getting-started.md) - What does ChameleonForms do for me? How are ChameleonForms forms structured? What terminology is used in ChameleonForms? Namespaces in `Views\web.config`. +* [Comparison](comparison.md) - See an example of a ChameleonForms form versus a traditional MVC form (HTML Helpers and Editor Templates) +* [Changing to Twitter Bootstrap 3 template](bootstrap-template.md) - Changing from the default template to the Twitter Bootstrap template +* [Automatically sentence case form labels](auto-sentence-case.md) - How to automatically sentence case your form labels without having to annotate them with `[DisplayName]` by adding a single line to `global.asax` +* [Field Configuration](field-configuration.md) - An overview of the common options available to configure a form field via the `IFieldConfiguration` interface +* [HTML Attributes](html-attributes.md) - An overview of how to define HTML attributes using the `HtmlAttributes` class * [Change the model type for HTML Helper for portions of your page](html-helper-context.md) -* [Create a form against a model type different from the page model](different-form-models.md) -* [Use partial views for repeated or abstracted form areas](partials.md) - -## Form structure -Examples for generating a form and each type of default component within the form. The following pages show both the ChameleonForms syntax, as well as the default generated HTML (which you can easily override to suit your own needs). - -* [Form](the-form.md) - How to output and configure the containing form -* [Message](the-message.md) - How to output and configure a message -* [Section](the-section.md) - How to output and configure a form section -* [Navigation](the-navigation.md) - How to output and configure a form navigation area and add buttons to it -* [Field](field.md) - How to output and configure templated fields - * [Field Element](field-element.md) - How to output the HTML for a field - * [Field Label](field-label.md) - How to output and configure field labels - * [Field Validation HTML](field-validation-html.md) - How to output validation messages for a field - -## Field types -* [Boolean fields](boolean.md) - Display booleans as a single checkbox, a select-list or a list of radio checkboxes -* [DateTime fields](datetime.md) - Display DateTimes as a text box including model binding and client-side validation that respects `[DisplayFormat]` - * [Client-side validation of DateTime fields](datetime-client-side-validation.md) - How to use `jquery.validate.unobtrusive.chameleon.js` -* [Enum fields](enum.md) - Display enums as drop-downs or a list of radio buttons -* [Multiple-select enum fields](multiple-enum.md) - Display enums as multi-select drop-downs or a list of checkboxes -* [List fields](list.md) - Display drop-downs or lists of radio buttons to allow users to select an item from a list -* [Multiple-select list fields](multiple-list.md) - Display multi-select drop-downs or lists of checkboxes to allow users to select multiple items from a list -* [Textarea fields](textarea.md) - Display textarea fields -* [File upload fields](file-upload.md) - Display file-upload fields -* [Password fields](password.md) - Display password fields -* [Default (``) fields](default-fields.md) - -## Advanced usage -* [Using different form templates](form-templates.md) -* [Creating custom form templates](custom-template.md) -* [Extending the field configuration](extending-field-configuration.md) -* [Extending the form components](extending-form-components.md) -* [Creating and using a custom field generator](custom-field-generator.md) -* [Creating and using custom field generator handlers](custom-field-generator-handlers.md) - -## ASP.NET MVC Posts - -If you need a hand getting started with ASP.NET MVC's form generation, model-binding and validation support then see the below. - -* [Building ASP.NET MVC Forms with Razor (ASP.NET MVC Foundations Series)](http://blog.michaelckennedy.net/2012/01/20/building-asp-net-mvc-forms-with-razor/) - -## Contributing -If you would like to contribute to this project then feel free to communicate with us via Twitter [@robdmoore](http://twitter.com/robdmoore) / [@mdaviesnet](http://twitter.com/mdaviesnet) or alternatively send a pull request / issue to this GitHub project. - -## Roadmap - -Feel free to check out our [Trello board](https://trello.com/board/chameleonforms/504df3392ad570121c36c3f7). It gives some idea as to the eventual goals we have for the project and the current backlog we are working against. Beware that it's pretty rough around the edges at the moment. +* [Create a form against a model type different from the page model](different-form-models.md) +* [Use partial views for repeated or abstracted form areas](partials.md) + +## Form structure +Examples for generating a form and each type of default component within the form. The following pages show both the ChameleonForms syntax, as well as the default generated HTML (which you can easily override to suit your own needs). + +* [Form](the-form.md) - How to output and configure the containing form +* [Message](the-message.md) - How to output and configure a message +* [Section](the-section.md) - How to output and configure a form section +* [Navigation](the-navigation.md) - How to output and configure a form navigation area and add buttons to it +* [Field](field.md) - How to output and configure templated fields + * [Field Element](field-element.md) - How to output the HTML for a field + * [Field Label](field-label.md) - How to output and configure field labels + * [Field Validation HTML](field-validation-html.md) - How to output validation messages for a field + +## Field types +* [Boolean fields](boolean.md) - Display booleans as a single checkbox, a select-list or a list of radio checkboxes +* [DateTime fields](datetime.md) - Display DateTimes as a text box including model binding and client-side validation that respects `[DisplayFormat]` + * [Client-side validation of DateTime fields](datetime-client-side-validation.md) - How to use `jquery.validate.unobtrusive.chameleon.js` +* [Enum fields](enum.md) - Display enums as drop-downs or a list of radio buttons +* [Flags enum fields](flags-enum.md) - Display flags enums as multi-select drop-downs or a list of checkboxes +* [Multiple-select enum fields](multiple-enum.md) - Display enums as multi-select drop-downs or a list of checkboxes +* [List fields](list.md) - Display drop-downs or lists of radio buttons to allow users to select an item from a list +* [Multiple-select list fields](multiple-list.md) - Display multi-select drop-downs or lists of checkboxes to allow users to select multiple items from a list +* [Textarea fields](textarea.md) - Display textarea fields +* [File upload fields](file-upload.md) - Display file-upload fields +* [Password fields](password.md) - Display password fields +* [Default (``) fields](default-fields.md) + +## Advanced usage +* [Using different form templates](form-templates.md) +* [Creating custom form templates](custom-template.md) +* [Extending the field configuration](extending-field-configuration.md) +* [Extending the form components](extending-form-components.md) +* [Creating and using a custom field generator](custom-field-generator.md) +* [Creating and using custom field generator handlers](custom-field-generator-handlers.md) + +## ASP.NET MVC Posts + +If you need a hand getting started with ASP.NET MVC's form generation, model-binding and validation support then see the below. + +* [Building ASP.NET MVC Forms with Razor (ASP.NET MVC Foundations Series)](http://blog.michaelckennedy.net/2012/01/20/building-asp-net-mvc-forms-with-razor/) + +## Contributing +If you would like to contribute to this project then feel free to communicate with us via Twitter [@robdmoore](http://twitter.com/robdmoore) / [@mdaviesnet](http://twitter.com/mdaviesnet) or alternatively send a pull request / issue to this GitHub project. + +## Roadmap + +Feel free to check out our [Trello board](https://trello.com/board/chameleonforms/504df3392ad570121c36c3f7). It gives some idea as to the eventual goals we have for the project and the current backlog we are working against. Beware that it's pretty rough around the edges at the moment. diff --git a/docs/multiple-enum.md b/docs/multiple-enum.md index 41ad033a..8321661a 100644 --- a/docs/multiple-enum.md +++ b/docs/multiple-enum.md @@ -1,7 +1,7 @@ Multiple-Select Enum Fields =========================== -If you want the user to specify multiple values from an enum you can use that enum type against any property with a type convertible to `IEnumerable<%enumType%>`, e.g.: +If you want the user to specify multiple values from an enum you can either use a [flags enum](flags-enum.md) or use a non-flags enum against any property with a type convertible to `IEnumerable<%enumType%>`, e.g.: ```csharp public enum MyEnum { ... } @@ -15,6 +15,8 @@ public List NullableEnumListField { get; set; } Note: as you will see below - there isn't much point in specifying a nullable enum for the enum type in the enumerable/list - we recommend you always use the enum type directly. +If you want the user to specify a single value from an enum then you can [use the enum type directly](enum.md). + Default HTML ------------ From 8f81876df5c602d6bf53d0dc9c7c4cbe7a5410e3 Mon Sep 17 00:00:00 2001 From: Rob Moore Date: Sun, 17 Jul 2016 18:38:40 +0800 Subject: [PATCH 5/6] Fixing broken test (oops) --- ...Tests.Use_correct_html_for_required_enum_field.approved.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_required_enum_field.approved.html b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_required_enum_field.approved.html index fe29da08..839e5087 100644 --- a/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_required_enum_field.approved.html +++ b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGenerator/FlagsEnumTests.Use_correct_html_for_required_enum_field.approved.html @@ -1,5 +1,5 @@  \ No newline at end of file From 1d1683c8499c6a7b1ae7429814079fa2eefadb04 Mon Sep 17 00:00:00 2001 From: Rob Moore Date: Mon, 18 Jul 2016 21:45:53 +0800 Subject: [PATCH 6/6] Removed the need to redundantly specify [Required, RequiredFlagsEnum] --- AppStart.cs.pp | 2 + .../Controllers/ExampleFormsController.cs | 4 +- ChameleonForms.Example/Global.asax.cs | 2 + .../DefaultFieldGeneratorTests.cs | 7 +- docs/flags-enum.md | 274 +++++++++--------- 5 files changed, 150 insertions(+), 139 deletions(-) diff --git a/AppStart.cs.pp b/AppStart.cs.pp index 8ff90bbc..10670f1b 100644 --- a/AppStart.cs.pp +++ b/AppStart.cs.pp @@ -1,3 +1,4 @@ +using ChameleonForms.Attributes; using ChameleonForms.ModelBinders; using System; using System.Web.Mvc; @@ -12,6 +13,7 @@ { System.Web.Mvc.ModelBinders.Binders.Add(typeof(DateTime), new DateTimeModelBinder()); System.Web.Mvc.ModelBinders.Binders.Add(typeof(DateTime?), new DateTimeModelBinder()); + DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredFlagsEnumAttribute), typeof(RequiredAttributeAdapter)); GetType().Assembly.GetTypes().Where(t => t.IsEnum && t.GetCustomAttributes(typeof(FlagsAttribute), false).Any()) .ToList().ForEach(t => { diff --git a/ChameleonForms.Example/Controllers/ExampleFormsController.cs b/ChameleonForms.Example/Controllers/ExampleFormsController.cs index 3c7998e5..10733a35 100644 --- a/ChameleonForms.Example/Controllers/ExampleFormsController.cs +++ b/ChameleonForms.Example/Controllers/ExampleFormsController.cs @@ -173,9 +173,9 @@ public IFieldConfiguration ModifyConfig(IFieldConfiguration config) public SomeEnum? RequiredNullableEnum { get; set; } public SomeEnum? OptionalEnum { get; set; } - [Required, RequiredFlagsEnum] + [RequiredFlagsEnum] public FlagsEnum RequiredFlagsEnum { get; set; } - [Required, RequiredFlagsEnum] + [RequiredFlagsEnum] public FlagsEnum? RequiredNullableFlagsEnum { get; set; } public FlagsEnum? OptionalFlagsEnum { get; set; } diff --git a/ChameleonForms.Example/Global.asax.cs b/ChameleonForms.Example/Global.asax.cs index 38173651..3dd60007 100644 --- a/ChameleonForms.Example/Global.asax.cs +++ b/ChameleonForms.Example/Global.asax.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Web.Mvc; using System.Web.Routing; +using ChameleonForms.Attributes; using ChameleonForms.Example.Controllers; using ChameleonForms.Example.Controllers.Filters; using ChameleonForms.ModelBinders; @@ -16,6 +17,7 @@ protected void Application_Start() HumanizedLabels.Register(); System.Web.Mvc.ModelBinders.Binders.Add(typeof(DateTime), new DateTimeModelBinder()); System.Web.Mvc.ModelBinders.Binders.Add(typeof(DateTime?), new DateTimeModelBinder()); + DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredFlagsEnumAttribute), typeof(RequiredAttributeAdapter)); typeof(ExampleFormsController).Assembly.GetTypes().Where(t => t.IsEnum && t.GetCustomAttributes(typeof(FlagsAttribute), false).Any()) .ToList().ForEach(t => { diff --git a/ChameleonForms.Tests/FieldGenerator/DefaultFieldGeneratorTests.cs b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGeneratorTests.cs index f261db75..c014783b 100644 --- a/ChameleonForms.Tests/FieldGenerator/DefaultFieldGeneratorTests.cs +++ b/ChameleonForms.Tests/FieldGenerator/DefaultFieldGeneratorTests.cs @@ -65,12 +65,12 @@ public TestFieldViewModel() public DateTime DateTimeWithGFormat { get; set; } public TestEnum RequiredEnum { get; set; } - [Required, RequiredFlagsEnum] + [RequiredFlagsEnum] public TestFlagsEnum RequiredFlagsEnum { get; set; } [Required] public TestEnum? RequiredNullableEnum { get; set; } - [Required, RequiredFlagsEnum] + [RequiredFlagsEnum] public TestFlagsEnum? RequiredNullableFlagsEnum { get; set; } [DisplayFormat(NullDisplayText = "Nothing to see here")] @@ -161,7 +161,7 @@ public class StringListItem public class ChildViewModel { public TestEnum RequiredChildEnum { get; set; } - [Required, RequiredFlagsEnum] + [RequiredFlagsEnum] public TestFlagsEnum RequiredChildFlagsEnum { get; set; } } @@ -187,6 +187,7 @@ protected DefaultFieldGenerator Arrange(Expression`](multiple-enum.md) or use a flags enum, e.g.: - -```csharp -[Flags] -public enum MyFlagsEnum -{ - ValueOne = 1 << 0, - ValueTwo = 1 << 1, - ValueThree = 1 << 2, - ... -} -... -[Required, RequiredFlagsEnum] -public MyFlagsEnum RequiredFlagsEnumWithZeroAsUnselectedValue { get; set; } - -[Required, RequiredFlagsEnum] -public MyFlagsEnum? RequiredFlagsEnumWithNullAsUnselectedValue { get; set; } - -public MyFlagsEnum? NonRequiredFlagsEnumAndNullAsUnselectedValue { get; set; } -``` - -Flags enums have a few rough edges on them if you aren't careful so it's a good idea to read the [guidance](https://msdn.microsoft.com/en-us/library/ms229062(v=vs.100).aspx) [for](https://msdn.microsoft.com/en-us/library/system.flagsattribute.aspx) how to use them. In particular, make sure that none of your values have a value of 0 and you explicitly assign integer values to all enum values in multiples of 2. - -If you want the user to specify a single value from an enum then you can [use the enum type directly](enum.md). - -Required validation -------------------- - -ASP.NET MVC's default validation doesn't pick up `0` for a flags enum as the field not being specified, thus you need to alter the validation for requires flags enums. ChameleonForms provides the `[RequiredFlagsEnum]` attribute to overcome that problem. You need to apply it alongside the usual `[Required]` attribute otherwise the client-side validation won't function as normal. This might look like: - -```csharp -[Required, RequiredFlagsEnum] -public MyFlagsEnum FlagsEnumField { get; set; } -``` - -Model binding -------------- - -The default MVC model binder does **not** correctly bind flags enum values. ChameleonForms provides the `FlagsEnumModelBinder` to assist with that. - -When you install ChameleonForms it should automatically register this model binder for all of the flags enum types registered in your MVC project within the `RegisterChameleonFormsComponents.cs` file. If you are upgrading from a pre version 3.0 version of ChameleonForms then the registration may not automatically add itself. Also, if you have view models / flags enums defined outside of your MVC project then the default registration won't work. This is the registration code we add (you may need to alter the assembly being scanned or alternatively explicitly register the model binder for the flags enums in question): - -```csharp +Flags Enum Fields +================= + +If you want the user to specify multiple values from an enum you can either use a [non-flags enum against any property with a type convertible to `IEnumerable<%enumType%>`](multiple-enum.md) or use a flags enum, e.g.: + +```csharp +[Flags] +public enum MyFlagsEnum +{ + ValueOne = 1 << 0, + ValueTwo = 1 << 1, + ValueThree = 1 << 2, + ... +} +... +[RequiredFlagsEnum] +public MyFlagsEnum RequiredFlagsEnumWithZeroAsUnselectedValue { get; set; } + +[RequiredFlagsEnum] +public MyFlagsEnum? RequiredFlagsEnumWithNullAsUnselectedValue { get; set; } + +public MyFlagsEnum? NonRequiredFlagsEnumAndNullAsUnselectedValue { get; set; } +``` + +Flags enums have a few rough edges on them if you aren't careful so it's a good idea to read the [guidance](https://msdn.microsoft.com/en-us/library/ms229062(v=vs.100).aspx) [for](https://msdn.microsoft.com/en-us/library/system.flagsattribute.aspx) how to use them. In particular, make sure that none of your values have a value of 0 and you explicitly assign integer values to all enum values in multiples of 2. + +If you want the user to specify a single value from an enum then you can [use the enum type directly](enum.md). + +Required validation +------------------- + +ASP.NET MVC's default validation doesn't pick up `0` for a flags enum as the field not being specified, thus you need to alter the validation for requires flags enums. ChameleonForms provides the `[RequiredFlagsEnum]` attribute to overcome that problem. This might look like: + +```csharp +[RequiredFlagsEnum] +public MyFlagsEnum FlagsEnumField { get; set; } +``` + +In order for this attribute to correctly apply client side validation you need to ensure the following call is made on your application start (this should be automatically added when installing ChameleonForms, but if you are upgrading from before version 3 it's possible it won't be added automatically): + +```csharp +DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredFlagsEnumAttribute), typeof(RequiredAttributeAdapter)); +``` + +Model binding +------------- + +The default MVC model binder does **not** correctly bind flags enum values. ChameleonForms provides the `FlagsEnumModelBinder` to assist with that. + +When you install ChameleonForms it should automatically register this model binder for all of the flags enum types registered in your MVC project within the `RegisterChameleonFormsComponents.cs` file. If you are upgrading from a pre version 3.0 version of ChameleonForms then the registration may not automatically add itself. Also, if you have view models / flags enums defined outside of your MVC project then the default registration won't work. This is the registration code we add (you may need to alter the assembly being scanned or alternatively explicitly register the model binder for the flags enums in question): + +```csharp GetType().Assembly.GetTypes().Where(t => t.IsEnum && t.GetCustomAttributes(typeof(FlagsAttribute), false).Any()) .ToList().ForEach(t => { System.Web.Mvc.ModelBinders.Binders.Add(t, new FlagsEnumModelBinder()); System.Web.Mvc.ModelBinders.Binders.Add(typeof(Nullable<>).MakeGenericType(t), new FlagsEnumModelBinder()); - }); -``` - -If registering a specifc flags enum type you can simply use: - -```csharp + }); +``` + +If registering a specifc flags enum type you can simply use: + +```csharp System.Web.Mvc.ModelBinders.Binders.Add(typeof(MyFlagsEnum), new FlagsEnumModelBinder()); - System.Web.Mvc.ModelBinders.Binders.Add(typeof(MyFlagsEnum?), new FlagsEnumModelBinder()); -``` - -Default HTML ------------- - -### Required nullable or non-nullable enum (multi-select drop-down with no empty option) - -```html - -``` - -### Non-Required nullable enum (multi-select drop-down with empty option) - -```html - -``` - -### Explanation and example - -Please see the explanation an example on the [Enum Field](enum#explanation-and-example) page. - -Configurability ---------------- - -### Display as list of checkboxes - -You can force a list of enums field to display as a list of checkboxes rather than a multi-select drop-down using the `AsCheckboxList` method on the Field Configuration, e.g.: - -```csharp -@s.FieldFor(m => m.FlagsEnum).AsCheckboxList() -``` - -This will change the default HTML for a both Required and non-Required list of enums (both nullable and non-nullable) fields as shown above to: - -```html -
    -%foreach enum value x with increment i % -
  • -%endforeach% -
-``` - -### Change the text description of none - -When you display a non-Required list of enums field as a drop-down you can change the text that is used to display the `none` value to the user. By default the text used is `None`. To change the text simply use the `WithNoneAs` method, e.g.: - -```csharp -@s.FieldFor(m => m.NonRequiredNullableFlagsEnum).WithNoneAs("No value") -``` - -This will change the default HTML for the non-Required drop-down list of enum field as shown above to: - -```html - -``` - -### Hide empty item -If you have a non-Required list of enums field then it will show the empty item and this item will be selected by default if no values are selected. If for some reason you want a non-Required list of enums field, but you would also like to hide the empty item you can do so with the `HideEmptyItem` method in the Field Configuration, e.g.: - -```csharp -@s.FieldFor(m => m.NullableEnumListField).HideEmptyItem() -``` - -This will change the default HTML for the non-Required drop-down list of enum field as shown above to: - -```html - + System.Web.Mvc.ModelBinders.Binders.Add(typeof(MyFlagsEnum?), new FlagsEnumModelBinder()); +``` + +Default HTML +------------ + +### Required nullable or non-nullable enum (multi-select drop-down with no empty option) + +```html + +``` + +### Non-Required nullable enum (multi-select drop-down with empty option) + +```html + +``` + +### Explanation and example + +Please see the explanation an example on the [Enum Field](enum#explanation-and-example) page. + +Configurability +--------------- + +### Display as list of checkboxes + +You can force a list of enums field to display as a list of checkboxes rather than a multi-select drop-down using the `AsCheckboxList` method on the Field Configuration, e.g.: + +```csharp +@s.FieldFor(m => m.FlagsEnum).AsCheckboxList() +``` + +This will change the default HTML for a both Required and non-Required list of enums (both nullable and non-nullable) fields as shown above to: + +```html +
    +%foreach enum value x with increment i % +
  • +%endforeach% +
+``` + +### Change the text description of none + +When you display a non-Required list of enums field as a drop-down you can change the text that is used to display the `none` value to the user. By default the text used is `None`. To change the text simply use the `WithNoneAs` method, e.g.: + +```csharp +@s.FieldFor(m => m.NonRequiredNullableFlagsEnum).WithNoneAs("No value") +``` + +This will change the default HTML for the non-Required drop-down list of enum field as shown above to: + +```html + +``` + +### Hide empty item +If you have a non-Required list of enums field then it will show the empty item and this item will be selected by default if no values are selected. If for some reason you want a non-Required list of enums field, but you would also like to hide the empty item you can do so with the `HideEmptyItem` method in the Field Configuration, e.g.: + +```csharp +@s.FieldFor(m => m.NullableEnumListField).HideEmptyItem() +``` + +This will change the default HTML for the non-Required drop-down list of enum field as shown above to: + +```html + ``` \ No newline at end of file