Skip to content

Commit

Permalink
Merge pull request #142 from MRCollective/flags-enum-support
Browse files Browse the repository at this point in the history
Flags enum support
  • Loading branch information
robdmoore authored Jul 19, 2016
2 parents 2fd8d48 + 1d1683c commit 1d6d9e1
Show file tree
Hide file tree
Showing 43 changed files with 992 additions and 208 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ _ReSharper.*
*.log
packages
*.received.*
.vs
8 changes: 8 additions & 0 deletions AppStart.cs.pp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using ChameleonForms.Attributes;
using ChameleonForms.ModelBinders;
using System;
using System.Web.Mvc;
Expand All @@ -12,6 +13,13 @@
{
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 =>
{
System.Web.Mvc.ModelBinders.Binders.Add(t, new FlagsEnumModelBinder());
System.Web.Mvc.ModelBinders.Binders.Add(typeof(Nullable<>).MakeGenericType(t), new FlagsEnumModelBinder());
});
}
}
}
13 changes: 13 additions & 0 deletions BREAKING_CHANGES.md
Original file line number Diff line number Diff line change
@@ -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
=============

Expand Down
3 changes: 3 additions & 0 deletions ChameleonForms.AcceptanceTests/Helpers/ObjectMother.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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> { SomeEnum.ValueWithDescription, SomeEnum.SomeOtherValue },
RequiredNullableEnums = new List<SomeEnum?> { SomeEnum.Value1 },
OptionalEnums = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using ChameleonForms.AcceptanceTests.Helpers;
using ChameleonForms.AcceptanceTests.ModelBinding.Pages;
using NUnit.Framework;
using TestStack.Seleno.Configuration;

namespace ChameleonForms.AcceptanceTests.ModelBinding
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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});
Expand All @@ -48,6 +50,19 @@ public object GetValueFromStrings(IEnumerable<string> 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));
}

Expand Down Expand Up @@ -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());
}
}

Expand All @@ -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;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,36 @@ 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<string> Values
{
get
{
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<object>()
.Where(e => (Convert.ToInt32(e) & Convert.ToInt32(_value)) != 0)
.Select(e => e.ToString());
return (_value as IEnumerable).Cast<object>()
.Select(o => new ModelFieldValue(o, _format))
.Select(v => v.Value);
Expand Down
18 changes: 18 additions & 0 deletions ChameleonForms.Example/Controllers/ExampleFormsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,12 @@ public IFieldConfiguration ModifyConfig(IFieldConfiguration config)
public SomeEnum? RequiredNullableEnum { get; set; }
public SomeEnum? OptionalEnum { get; set; }

[RequiredFlagsEnum]
public FlagsEnum RequiredFlagsEnum { get; set; }
[RequiredFlagsEnum]
public FlagsEnum? RequiredNullableFlagsEnum { get; set; }
public FlagsEnum? OptionalFlagsEnum { get; set; }

[Required]
public IEnumerable<SomeEnum> RequiredEnums { get; set; }
[Required]
Expand Down Expand Up @@ -246,6 +252,8 @@ public ViewModelExample()

public string NestedField { get; set; }

public FlagsEnum FlagsEnums { get; set; }

public SomeEnum SomeEnum { get; set; }

public List<SomeEnum> SomeEnums { get; set; }
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions ChameleonForms.Example/Global.asax.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System;
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;

Expand All @@ -14,6 +17,13 @@ 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 =>
{
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());
}
}
Expand Down
1 change: 1 addition & 0 deletions ChameleonForms.Example/Views/ExampleForms/Form1.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<p>@f.FieldElementFor(m => m.RequiredStringField).TabIndex(4)</p>
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)))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
41 changes: 41 additions & 0 deletions ChameleonForms.Tests/Attributes/RequiredFlagsEnumAttributeTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
3 changes: 3 additions & 0 deletions ChameleonForms.Tests/ChameleonForms.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Attributes\ExistsInAttributeTests.cs" />
<Compile Include="Attributes\RequiredFlagsEnumAttributeTests.cs" />
<Compile Include="Component\Config\FieldConfigurationTests.cs" />
<Compile Include="Component\NavigationTests.cs" />
<Compile Include="Component\MessageTests.cs" />
Expand All @@ -139,6 +140,7 @@
<Compile Include="FieldGenerator\DefaultFieldGeneratorTests.cs" />
<Compile Include="FieldGenerator\DefaultFieldGenerator\BooleanTests.cs" />
<Compile Include="FieldGenerator\DefaultFieldGenerator\DateTimeTests.cs" />
<Compile Include="FieldGenerator\DefaultFieldGenerator\FlagsEnumTests.cs" />
<Compile Include="FieldGenerator\DefaultFieldGenerator\EnumTests.cs" />
<Compile Include="FieldGenerator\DefaultFieldGenerator\FieldGeneratorTests.cs" />
<Compile Include="FieldGenerator\DefaultFieldGenerator\InputTests.cs" />
Expand All @@ -158,6 +160,7 @@
<Compile Include="Helpers\ChangeCulture.cs" />
<Compile Include="HtmlHelperExtensionsTests.cs" />
<Compile Include="HumanizedLabelsTests.cs" />
<Compile Include="ModelBinders\FlagsEnumModelBinderShould.cs" />
<Compile Include="ModelBinders\DateTimeModelBinderShould.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Templates\LazyHtmlAttributesTests.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<ul>
<li>
<input class="input-validation-error" data-attr="value" data-val="true" data-val-required="The RequiredChildFlagsEnum field is required." id="Child_RequiredChildFlagsEnum_1" name="Child.RequiredChildFlagsEnum" type="checkbox" value="Simplevalue" />
<label for="Child_RequiredChildFlagsEnum_1">Simplevalue</label>
</li>
<li>
<input class="input-validation-error" data-attr="value" id="Child_RequiredChildFlagsEnum_2" name="Child.RequiredChildFlagsEnum" type="checkbox" value="ValueWithDescriptionAttribute" />
<label for="Child_RequiredChildFlagsEnum_2">Description attr text</label>
</li>
<li>
<input class="input-validation-error" data-attr="value" id="Child_RequiredChildFlagsEnum_3" name="Child.RequiredChildFlagsEnum" type="checkbox" value="ValueWithMultpipleWordsAndNoDescriptionAttribute" />
<label for="Child_RequiredChildFlagsEnum_3">Value with multpiple words and no description attribute</label>
</li>
</ul>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<select class="input-validation-error" data-attr="value" data-val="true" data-val-required="The RequiredFlagsEnum field is required." id="RequiredFlagsEnum" multiple="multiple" name="RequiredFlagsEnum">
<option value="ValueWithDescriptionAttribute">Description attr text</option>
<option value="ValueWithMultpipleWordsAndNoDescriptionAttribute">Value with multpiple words and no description attribute</option>
</select>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<label for="RequiredFlagsEnum">RequiredFlagsEnum</label>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
RequiredFlagsEnum
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<strong>lol</strong>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<select class="input-validation-error" data-attr="value" data-val="true" data-val-required="The RequiredNullableFlagsEnum field is required." id="RequiredNullableFlagsEnum" multiple="multiple" name="RequiredNullableFlagsEnum">
<option value="Simplevalue">Simplevalue</option>
<option value="ValueWithDescriptionAttribute">Description attr text</option>
<option value="ValueWithMultpipleWordsAndNoDescriptionAttribute">Value with multpiple words and no description attribute</option>
</select>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<select class="input-validation-error" data-attr="value" id="OptionalFlagsEnum" multiple="multiple" name="OptionalFlagsEnum">
<option selected="selected" value="">None</option>
<option value="Simplevalue">Simplevalue</option>
<option value="ValueWithDescriptionAttribute">Description attr text</option>
<option value="ValueWithMultpipleWordsAndNoDescriptionAttribute">Value with multpiple words and no description attribute</option>
</select>
Loading

0 comments on commit 1d6d9e1

Please sign in to comment.