diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index 2fb5f2c9..3d0e82f7 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -4,6 +4,19 @@ ChameleonForms Breaking Changes Version 2.0.0 ============= +Deprecated `WithoutLabel` method on `IFieldConfiguration`. It still works (for now), but the method has been marked with the `[Obsolete]` attribute. + +### Reason + +The method has been renamed to `WithoutLabelElement` since it more closely reflects what the method does. + +### Workaround + +Change all instances of `WithoutLabel` to `WithoutLabelElement`. + +Version 2.0.0 +============= + Remove the `ReadOnlyConfiguration` public class. ### Reason diff --git a/ChameleonForms.AcceptanceTests/ChameleonForms.AcceptanceTests.csproj b/ChameleonForms.AcceptanceTests/ChameleonForms.AcceptanceTests.csproj index 0cb7442d..da6e3fb5 100644 --- a/ChameleonForms.AcceptanceTests/ChameleonForms.AcceptanceTests.csproj +++ b/ChameleonForms.AcceptanceTests/ChameleonForms.AcceptanceTests.csproj @@ -32,6 +32,14 @@ 4 + + ..\packages\ApprovalTests.3.0.9\lib\net40\ApprovalTests.dll + True + + + ..\packages\ApprovalUtilities.3.0.9\lib\net35\ApprovalUtilities.dll + True + False ..\packages\Castle.Core.3.3.0\lib\net40-client\Castle.Core.dll @@ -130,6 +138,7 @@ + diff --git a/ChameleonForms.AcceptanceTests/PartialForTests.Should_render_correctly_when_used_via_form_or_section_and_when_used_for_top_level_property_or_sub_property.approved.html b/ChameleonForms.AcceptanceTests/PartialForTests.Should_render_correctly_when_used_via_form_or_section_and_when_used_for_top_level_property_or_sub_property.approved.html new file mode 100644 index 00000000..fe66128c --- /dev/null +++ b/ChameleonForms.AcceptanceTests/PartialForTests.Should_render_correctly_when_used_via_form_or_section_and_when_used_for_top_level_property_or_sub_property.approved.html @@ -0,0 +1,121 @@ +Partials.cshtml + +@model ViewModelExample + +@{ + ViewBag.Title = "Partials"; +} + +

Partials

+ +@using (var f = Html.BeginChameleonForm()) +{ + @f.Partial("_ParentPartial") + @f.PartialFor(m => m.Child, "_ChildPartial") + + using (var s = f.BeginSection("This is in the parent view")) + { + @s.FieldFor(m => m.Decimal).Append("in parent view") + @s.Partial("_ParentPartial") + @s.FieldFor(m => m.ListId).Append("in parent view") + @s.PartialFor(m => m.Child, "_ChildPartial") + @s.FieldFor(m => m.SomeCheckbox).Append(" in parent view") + } + + using (var n = f.BeginNavigation()) + { + @n.Submit("Submit") + } +} + +===== + +_ParentPartial.cshtml + +@model ViewModelExample + +@if (this.IsInFormSection()) +{ + @:@this.FormSection().FieldFor(m => m.TextAreaField).Append("from partial against top-level model") +} +else +{ + using (var s = this.Form().BeginSection("This is from a form-level partial against the top-level model")) { + @s.FieldFor(m => m.Int) + } +} + +===== + +_ChildPartial.cshtml + +@model ChildViewModel + +@if (this.IsInFormSection()) +{ + @:@this.FormSection().FieldFor(m => m.ChildField).Append("From partial against child model") +} +else +{ + using (var s = this.Form().BeginSection("This is from a form-level partial against a child model")) { + @s.FieldFor(m => m.SomeEnum) + } +} + +===== + +Rendered Source + +
+ This is from a form-level partial against the top-level model +
+
*
+
+ +
+
+
+
+ This is from a form-level partial against a child model +
+
*
+
+ +
+
+
+
+ This is in the parent view +
+
*
+
+ in parent view +
+
+
+ from partial against top-level model +
+ +
*
+
+ in parent view +
+
+
+ From partial against child model +
+ +
*
+
+ in parent view +
+
+
+
+
+
\ No newline at end of file diff --git a/ChameleonForms.AcceptanceTests/PartialForTests.cs b/ChameleonForms.AcceptanceTests/PartialForTests.cs new file mode 100644 index 00000000..97f9adff --- /dev/null +++ b/ChameleonForms.AcceptanceTests/PartialForTests.cs @@ -0,0 +1,48 @@ +using System; +using System.CodeDom; +using System.IO; +using System.Net.Http; +using System.Text.RegularExpressions; +using ApprovalTests.Html; +using ApprovalTests.Reporters; +using NUnit.Framework; +using OpenQA.Selenium; +using OpenQA.Selenium.Support.UI; + +namespace ChameleonForms.AcceptanceTests +{ + /// + /// Loading partial views is very difficult to test by unit testing. + /// + [UseReporter(typeof(DiffReporter))] + public class PartialForTests + { + [Test] + public void Should_render_correctly_when_used_via_form_or_section_and_when_used_for_top_level_property_or_sub_property() + { + var renderedSource = GetRenederedSource("/ExampleForms/Partials"); + HtmlApprovals.VerifyHtml(string.Format("Partials.cshtml\r\n\r\n{0}\r\n=====\r\n\r\n_ParentPartial.cshtml\r\n\r\n{1}\r\n=====\r\n\r\n_ChildPartial.cshtml\r\n\r\n{2}\r\n=====\r\n\r\nRendered Source\r\n\r\n{3}", + GetViewContents("Partials"), + GetViewContents("_ParentPartial"), + GetViewContents("_ChildPartial"), + renderedSource)); + } + + private string GetRenederedSource(string url) + { + Host.Instance.Application.Browser.Navigate().GoToUrl(string.Format("http://localhost:12345{0}", url)); + new WebDriverWait(Host.Instance.Application.Browser, TimeSpan.FromSeconds(5)) + .Until(b => b.FindElement(By.Id("Int"))); + var renderedSource = Host.Instance.Application.Browser.PageSource; + var getFormContent = new Regex(@".*?().*", RegexOptions.Multiline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + return getFormContent.Match(renderedSource).Groups[1].Value; + } + + private string GetViewContents(string viewPath) + { + return File.ReadAllText(string.Format(ViewPath, viewPath)); + } + + private static readonly string ViewPath = Path.Combine(Path.GetDirectoryName(typeof(PartialForTests).Assembly.CodeBase.Replace("file:///", "")), "..", "..", "..", "ChameleonForms.Example", "Views", "ExampleForms", "{0}.cshtml"); + } +} diff --git a/ChameleonForms.AcceptanceTests/packages.config b/ChameleonForms.AcceptanceTests/packages.config index 3c06ea16..b3753e07 100644 --- a/ChameleonForms.AcceptanceTests/packages.config +++ b/ChameleonForms.AcceptanceTests/packages.config @@ -1,5 +1,7 @@  + + diff --git a/ChameleonForms.Example/ChameleonForms.Example.csproj b/ChameleonForms.Example/ChameleonForms.Example.csproj index 26c08f01..9ff6db1d 100644 --- a/ChameleonForms.Example/ChameleonForms.Example.csproj +++ b/ChameleonForms.Example/ChameleonForms.Example.csproj @@ -130,6 +130,7 @@ +
@@ -200,6 +201,8 @@ + + diff --git a/ChameleonForms.Example/Controllers/ExampleFormsController.cs b/ChameleonForms.Example/Controllers/ExampleFormsController.cs index 53e384df..c0093d13 100644 --- a/ChameleonForms.Example/Controllers/ExampleFormsController.cs +++ b/ChameleonForms.Example/Controllers/ExampleFormsController.cs @@ -76,6 +76,17 @@ public ActionResult Buttons() { return View(); } + + public ActionResult Partials() + { + return View(new ViewModelExample{TextAreaField = "Initial value"}); + } + + [HttpPost] + public ActionResult Partials(ViewModelExample vm) + { + return View(vm); + } } public class ModelBindingViewModel @@ -158,7 +169,7 @@ public IFieldConfiguration ModifyConfig(IFieldConfiguration config) // These are tested in addition to the other list tests as there - // was an bug manifesting when using an array of a templated type. + // was a bug manifesting when using an array of a templated type. [ReadOnly(true)] public Tuple[] ChoicesAsTuples { get; set; } diff --git a/ChameleonForms.Example/Views/ExampleForms/Partials.cshtml b/ChameleonForms.Example/Views/ExampleForms/Partials.cshtml new file mode 100644 index 00000000..b0dacea4 --- /dev/null +++ b/ChameleonForms.Example/Views/ExampleForms/Partials.cshtml @@ -0,0 +1,27 @@ +@model ViewModelExample + +@{ + ViewBag.Title = "Partials"; +} + +

Partials

+ +@using (var f = Html.BeginChameleonForm()) +{ + @f.Partial("_ParentPartial") + @f.PartialFor(m => m.Child, "_ChildPartial") + + using (var s = f.BeginSection("This is in the parent view")) + { + @s.FieldFor(m => m.Decimal).Append("in parent view") + @s.Partial("_ParentPartial") + @s.FieldFor(m => m.ListId).Append("in parent view") + @s.PartialFor(m => m.Child, "_ChildPartial") + @s.FieldFor(m => m.SomeCheckbox).Append(" in parent view") + } + + using (var n = f.BeginNavigation()) + { + @n.Submit("Submit") + } +} diff --git a/ChameleonForms.Example/Views/ExampleForms/_ChildPartial.cshtml b/ChameleonForms.Example/Views/ExampleForms/_ChildPartial.cshtml new file mode 100644 index 00000000..f724ccf0 --- /dev/null +++ b/ChameleonForms.Example/Views/ExampleForms/_ChildPartial.cshtml @@ -0,0 +1,12 @@ +@model ChildViewModel + +@if (this.IsInFormSection()) +{ + @:@this.FormSection().FieldFor(m => m.ChildField).Append("From partial against child model") +} +else +{ + using (var s = this.Form().BeginSection("This is from a form-level partial against a child model")) { + @s.FieldFor(m => m.SomeEnum) + } +} diff --git a/ChameleonForms.Example/Views/ExampleForms/_ParentPartial.cshtml b/ChameleonForms.Example/Views/ExampleForms/_ParentPartial.cshtml new file mode 100644 index 00000000..7ef51153 --- /dev/null +++ b/ChameleonForms.Example/Views/ExampleForms/_ParentPartial.cshtml @@ -0,0 +1,12 @@ +@model ViewModelExample + +@if (this.IsInFormSection()) +{ + @:@this.FormSection().FieldFor(m => m.TextAreaField).Append("from partial against top-level model") +} +else +{ + using (var s = this.Form().BeginSection("This is from a form-level partial against the top-level model")) { + @s.FieldFor(m => m.Int) + } +} diff --git a/ChameleonForms.Example/Views/Home/Index.cshtml b/ChameleonForms.Example/Views/Home/Index.cshtml index 4806c280..9237944b 100644 --- a/ChameleonForms.Example/Views/Home/Index.cshtml +++ b/ChameleonForms.Example/Views/Home/Index.cshtml @@ -27,6 +27,7 @@
  • @Html.ActionLink("Null model with list", "NullModelWithList", "ExampleForms")
  • @Html.ActionLink("Null list", "NullList", "ExampleForms")
  • @Html.ActionLink("Buttons", "Buttons", "ExampleForms")
  • +
  • @Html.ActionLink("Partials", "Partials", "ExampleForms")
  • Random forms and UI tests

    diff --git a/ChameleonForms.Tests/ChameleonForms.Tests.csproj b/ChameleonForms.Tests/ChameleonForms.Tests.csproj index b402292f..e86006ca 100644 --- a/ChameleonForms.Tests/ChameleonForms.Tests.csproj +++ b/ChameleonForms.Tests/ChameleonForms.Tests.csproj @@ -34,12 +34,12 @@ - False - ..\packages\ApprovalTests.3.0.01\lib\net40\ApprovalTests.dll + ..\packages\ApprovalTests.3.0.9\lib\net40\ApprovalTests.dll + True - False - ..\packages\ApprovalUtilities.3.0.01\lib\net35\ApprovalUtilities.dll + ..\packages\ApprovalUtilities.3.0.9\lib\net35\ApprovalUtilities.dll + True False diff --git a/ChameleonForms.Tests/packages.config b/ChameleonForms.Tests/packages.config index aacd8c15..2e8c8887 100644 --- a/ChameleonForms.Tests/packages.config +++ b/ChameleonForms.Tests/packages.config @@ -1,7 +1,7 @@  - - + + diff --git a/ChameleonForms/ChameleonForms.csproj b/ChameleonForms/ChameleonForms.csproj index 0000c9ca..8eb442cc 100644 --- a/ChameleonForms/ChameleonForms.csproj +++ b/ChameleonForms/ChameleonForms.csproj @@ -90,6 +90,9 @@ + + + @@ -129,6 +132,9 @@ TwitterBootstrapHtmlHelpers.cshtml + + + diff --git a/ChameleonForms/Component/Field.cs b/ChameleonForms/Component/Field.cs index 863c9009..3c7ce862 100644 --- a/ChameleonForms/Component/Field.cs +++ b/ChameleonForms/Component/Field.cs @@ -85,7 +85,7 @@ public static class FieldExtensions /// The section the field is being created in /// A lamdba expression to identify the field to render the field for /// A field configuration object that allows you to configure the field - public static IFieldConfiguration FieldFor(this Section section, Expression> property) + public static IFieldConfiguration FieldFor(this ISection section, Expression> property) { var fc = new FieldConfiguration(); // ReSharper disable ObjectCreationAsStatement @@ -108,7 +108,7 @@ public static IFieldConfiguration FieldFor(this Section secti /// A lamdba expression to identify the field to render the field for /// Any configuration information for the field /// The form field - public static Field BeginFieldFor(this Section section, Expression> property, IFieldConfiguration config = null) + public static Field BeginFieldFor(this ISection section, Expression> property, IFieldConfiguration config = null) { return new Field(section.Form, true, section.Form.GetFieldGenerator(property), config); } diff --git a/ChameleonForms/Component/Section.cs b/ChameleonForms/Component/Section.cs index 57c149f7..0a4f068b 100644 --- a/ChameleonForms/Component/Section.cs +++ b/ChameleonForms/Component/Section.cs @@ -1,16 +1,36 @@ -using System.Web; +using System; +using System.Linq.Expressions; +using System.Web; using System.Web.Mvc; +using System.Web.Mvc.Html; using ChameleonForms.Component.Config; -using ChameleonForms.Templates; +using JetBrains.Annotations; namespace ChameleonForms.Component { + /// + /// Interface for a modeless cast of a ChameleonForms Section. + /// + public interface ISection + { + /// + /// Returns a section with the same characteristics as the current section, but using the given partial form. + /// + /// The model type of the partial view + /// A section with the same characteristics as the current section, but using the given partial form + ISection CreatePartialSection(IForm partialModelForm); + } + + /// + /// Tagging interface for a ChameleonForms Section with a model type. + /// + public interface ISection : IFormComponent {} + /// /// Wraps the output of a form section. /// /// The view model type for the current view - - public class Section : FormComponent + public class Section : FormComponent, ISection, ISection { private readonly IHtmlString _heading; private readonly bool _nested; @@ -61,6 +81,12 @@ public override IHtmlString End() { return _nested ? Form.Template.EndNestedSection() : Form.Template.EndSection(); } + + /// + public ISection CreatePartialSection(IForm partialModelForm) + { + return new PartialViewSection(partialModelForm); + } } /// @@ -107,5 +133,37 @@ public static Section BeginSection(this Section section, { return new Section(section.Form, heading.ToHtml(), true, leadingHtml, htmlAttributes); } + + /// + /// Renders the given partial in the context of the parent model. + /// + /// The form model type + /// The current section + /// The name of the partial view to render + /// The HTML for the rendered partial + public static IHtmlString Partial(this ISection section, [AspMvcPartialView] string partialViewName) + { + return PartialFor(section, m => m, partialViewName); + } + + /// + /// Renders the given partial in the context of the given property. + /// Use PartialFor(m => m) to render a partial for the model itself rather than a child property. + /// + /// The form model type + /// The type of the model property to use for the partial model + /// The current section + /// The property to use for the partial model + /// The name of the partial view to render + /// The HTML for the rendered partial + public static IHtmlString PartialFor(this ISection section, Expression> partialModelProperty, [AspMvcPartialView] string partialViewName) + { + var formModel = (TModel)section.Form.HtmlHelper.ViewData.ModelMetadata.Model; + var viewData = new ViewDataDictionary(section.Form.HtmlHelper.ViewData); + viewData[WebViewPageExtensions.PartialViewModelExpressionViewDataKey] = partialModelProperty; + viewData[WebViewPageExtensions.CurrentFormViewDataKey] = section.Form; + viewData[WebViewPageExtensions.CurrentFormSectionViewDataKey] = section; + return section.Form.HtmlHelper.Partial(partialViewName, partialModelProperty.Compile().Invoke(formModel), viewData); + } } } diff --git a/ChameleonForms/Form.cs b/ChameleonForms/Form.cs index 70a6cf62..3e83af71 100644 --- a/ChameleonForms/Form.cs +++ b/ChameleonForms/Form.cs @@ -2,17 +2,34 @@ using System.Linq.Expressions; using System.Web; using System.Web.Mvc; +using System.Web.Mvc.Html; using ChameleonForms.Enums; using ChameleonForms.FieldGenerators; using ChameleonForms.Templates; +using JetBrains.Annotations; namespace ChameleonForms { + /// + /// Interface for a modeless cast of a Chameleon Form. + /// + public interface IForm + { + /// + /// Returns a wrapped for the given partial view information. + /// + /// The model type of the partial view + /// The expression that identifies the partial model + /// The HTML Helper from the partial view + /// The PartialViewForm wrapping the original form + IForm CreatePartialForm(object partialModelExpression, HtmlHelper partialViewHelper); + } + /// /// Interface for a Chameleon Form. /// /// The view model type for the current view - public interface IForm : IDisposable + public interface IForm : IForm, IDisposable { /// /// The HTML helper for the current view. @@ -82,6 +99,13 @@ public void Dispose() { Write(Template.EndForm()); } + + /// + public IForm CreatePartialForm(object partialModelExpression, HtmlHelper partialViewHelper) + { + var partialModelAsExpression = partialModelExpression as Expression>; + return new PartialViewForm(this, partialViewHelper, partialModelAsExpression); + } } /// @@ -108,5 +132,36 @@ public static IForm BeginChameleonForm(this HtmlHelper h { return new Form(helper, FormTemplate.Default, action, method, htmlAttributes, enctype); } + + /// + /// Renders the given partial in the context of the parent model. + /// + /// The form model type + /// The form + /// The name of the partial view to render + /// The HTML for the rendered partial + public static IHtmlString Partial(this IForm form, [AspMvcPartialView] string partialViewName) + { + return PartialFor(form, m => m, partialViewName); + } + + /// + /// Renders the given partial in the context of the given property. + /// Use PartialFor(m => m, ...) pr Partial(...) to render a partial for the model itself rather than a child property. + /// + /// The form model type + /// The type of the model property to use for the partial model + /// The form + /// The property to use for the partial model + /// The name of the partial view to render + /// The HTML for the rendered partial + public static IHtmlString PartialFor(this IForm form, Expression> partialModelProperty, [AspMvcPartialView] string partialViewName) + { + var formModel = (TModel) form.HtmlHelper.ViewData.ModelMetadata.Model; + var viewData = new ViewDataDictionary(form.HtmlHelper.ViewData); + viewData[WebViewPageExtensions.PartialViewModelExpressionViewDataKey] = partialModelProperty; + viewData[WebViewPageExtensions.CurrentFormViewDataKey] = form; + return form.HtmlHelper.Partial(partialViewName, partialModelProperty.Compile().Invoke(formModel), viewData); + } } } \ No newline at end of file diff --git a/ChameleonForms/PartialViewForm.cs b/ChameleonForms/PartialViewForm.cs new file mode 100644 index 00000000..42b1a3f7 --- /dev/null +++ b/ChameleonForms/PartialViewForm.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq.Expressions; +using System.Web; +using System.Web.Mvc; +using ChameleonForms.FieldGenerators; +using ChameleonForms.Templates; +using ChameleonForms.Utils; + +namespace ChameleonForms +{ + /// + /// Form that looks like the parent form, but writes to the text writer for the partial otherwise the output is out of order. + /// + internal class PartialViewForm : IForm + { + private readonly IForm _parentForm; + private readonly HtmlHelper _partialViewHtmlHelper; + private readonly Expression> _partialModelProperty; + + public PartialViewForm(IForm parentForm, HtmlHelper partialViewHtmlHelper, Expression> partialModelProperty) + { + _parentForm = parentForm; + _partialViewHtmlHelper = partialViewHtmlHelper; + _partialModelProperty = partialModelProperty; + } + + public HtmlHelper HtmlHelper { get { return _partialViewHtmlHelper; } } + public IFormTemplate Template { get { return _parentForm.Template; } } + + public void Write(IHtmlString htmlString) + { + _partialViewHtmlHelper.ViewContext.Writer.Write(htmlString); + } + + public IFieldGenerator GetFieldGenerator(Expression> property) + { + using (new SwapHtmlHelperWriter(_parentForm.HtmlHelper, _partialViewHtmlHelper.ViewContext.Writer)) + { + return new DefaultFieldGenerator(_parentForm.HtmlHelper, _partialModelProperty.Combine(property), Template); + } + } + + public IForm CreatePartialForm(object childPartialModelExpression, HtmlHelper partialViewHelper) + { + var childPartialModelAsExpression = childPartialModelExpression as Expression>; + var partialModelAsExpression = _partialModelProperty.Combine(childPartialModelAsExpression); + return new PartialViewForm(_parentForm, partialViewHelper, partialModelAsExpression); + } + + public void Dispose() {} + } +} diff --git a/ChameleonForms/PartialViewSection.cs b/ChameleonForms/PartialViewSection.cs new file mode 100644 index 00000000..2da882f3 --- /dev/null +++ b/ChameleonForms/PartialViewSection.cs @@ -0,0 +1,14 @@ +using ChameleonForms.Component; + +namespace ChameleonForms +{ + internal class PartialViewSection : ISection + { + public PartialViewSection(IForm form) + { + Form = form; + } + + public IForm Form { get; private set; } + } +} diff --git a/ChameleonForms/ReSharper/ReSharperAnnotations.cs b/ChameleonForms/ReSharper/ReSharperAnnotations.cs new file mode 100644 index 00000000..aae6889d --- /dev/null +++ b/ChameleonForms/ReSharper/ReSharperAnnotations.cs @@ -0,0 +1,55 @@ +using System; +#pragma warning disable 1591 +// ReSharper disable UnusedMember.Global +// ReSharper disable UnusedParameter.Local +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable IntroduceOptionalParameters.Global +// ReSharper disable MemberCanBeProtected.Global +// ReSharper disable InconsistentNaming + +// ReSharper disable once CheckNamespace +namespace JetBrains.Annotations +{ + /// + /// Indicates that the value of the marked element could never be null + /// + /// + /// [NotNull] public object Foo() { + /// return null; // Warning: Possible 'null' assignment + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | + AttributeTargets.Property | AttributeTargets.Delegate | + AttributeTargets.Field, AllowMultiple = false, Inherited = true)] + internal sealed class NotNullAttribute : Attribute { } + + /// + /// Indicates that a parameter is a path to a file or a folder + /// within a web project. Path can be relative or absolute, + /// starting from web root (~) + /// + [AttributeUsage(AttributeTargets.Parameter)] + internal class PathReferenceAttribute : Attribute + { + public PathReferenceAttribute() { } + public PathReferenceAttribute([PathReference] string basePath) + { + BasePath = basePath; + } + + [NotNull] + public string BasePath { get; private set; } + } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that + /// the parameter is an MVC partial view. If applied to a method, + /// the MVC partial view name is calculated implicitly from the context. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.RenderPartialExtensions.RenderPartial(HtmlHelper, String) + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] + internal sealed class AspMvcPartialViewAttribute : PathReferenceAttribute { } +} \ No newline at end of file diff --git a/ChameleonForms/Utils/ExpressionExtensions.cs b/ChameleonForms/Utils/ExpressionExtensions.cs new file mode 100644 index 00000000..04cbee3f --- /dev/null +++ b/ChameleonForms/Utils/ExpressionExtensions.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq.Expressions; + +namespace ChameleonForms.Utils +{ + // http://stackoverflow.com/questions/10898800/combine-two-linq-expressions-to-inject-navigation-property + static class ExpressionExtensions + { + public static Expression> Combine(this Expression> parent, Expression> nav) + { + var param = Expression.Parameter(typeof(T), "x"); + var visitor = new ReplacementVisitor(parent.Parameters[0], param); + var newParentBody = visitor.Visit(parent.Body); + visitor = new ReplacementVisitor(nav.Parameters[0], newParentBody); + var body = visitor.Visit(nav.Body); + return Expression.Lambda>(body, param); + } + } + + class ReplacementVisitor : ExpressionVisitor + { + private readonly Expression _oldExpr; + private readonly Expression _newExpr; + + public ReplacementVisitor(Expression oldExpr, Expression newExpr) + { + _oldExpr = oldExpr; + _newExpr = newExpr; + } + + public override Expression Visit(Expression node) + { + return node == _oldExpr ? _newExpr : base.Visit(node); + } + } +} diff --git a/ChameleonForms/Utils/SwapHtmlHelperWriter.cs b/ChameleonForms/Utils/SwapHtmlHelperWriter.cs new file mode 100644 index 00000000..323d9b8d --- /dev/null +++ b/ChameleonForms/Utils/SwapHtmlHelperWriter.cs @@ -0,0 +1,24 @@ +using System; +using System.IO; +using System.Web.Mvc; + +namespace ChameleonForms.Utils +{ + internal class SwapHtmlHelperWriter : IDisposable + { + private readonly HtmlHelper _htmlHelper; + private readonly TextWriter _oldWriter; + + public SwapHtmlHelperWriter(HtmlHelper htmlHelper, TextWriter writer) + { + _htmlHelper = htmlHelper; + _oldWriter = htmlHelper.ViewContext.Writer; + htmlHelper.ViewContext.Writer = writer; + } + + public void Dispose() + { + _htmlHelper.ViewContext.Writer = _oldWriter; + } + } +} \ No newline at end of file diff --git a/ChameleonForms/WebViewPageExtensions.cs b/ChameleonForms/WebViewPageExtensions.cs new file mode 100644 index 00000000..5c369997 --- /dev/null +++ b/ChameleonForms/WebViewPageExtensions.cs @@ -0,0 +1,89 @@ +using System; +using System.Linq.Expressions; +using System.Web.Mvc; +using ChameleonForms.Component; + +namespace ChameleonForms +{ + /// + /// Extension methods against WebViewPage. + /// + public static class WebViewPageExtensions + { + /// + /// Key to use in ViewData to set and retrieve the current partial view model expression. + /// + public const string PartialViewModelExpressionViewDataKey = "PartialViewModelExpression"; + + /// + /// Get view model expression when inside a partial view. + /// + /// View model type of the partial view + /// View page for partial view + /// current partial view model expression + public static object PartialModelExpression(this WebViewPage partial) + { + object expression; + if (!partial.ViewData.TryGetValue(PartialViewModelExpressionViewDataKey, out expression)) + { + throw new InvalidOperationException("Not currently inside a form partial view."); + } + + return expression; + } + + /// + /// Key to use in ViewData to set and retrieve the current form section. + /// + public const string CurrentFormSectionViewDataKey = "CurrentChameleonFormSection"; + + /// + /// Get current form section when inside a partial view. + /// + /// View model of the partial view + /// View page for partial view + /// Current form section + public static ISection FormSection(this WebViewPage partial) + { + object currentSection; + if (!partial.ViewData.TryGetValue(CurrentFormSectionViewDataKey, out currentSection)) + { + throw new InvalidOperationException("Not currently inside a form section."); + } + + return (currentSection as ISection).CreatePartialSection(Form(partial)); + } + + /// + /// Key to use in ViewData to set and retrieve the current form. + /// + public const string CurrentFormViewDataKey = "CurrentChameleonForm"; + + /// + /// Get current form when inside a partial view. + /// + /// View model of the partial view + /// View page for partial view + /// Current form + public static IForm Form(this WebViewPage partial) + { + object currentForm; + if (!partial.ViewData.TryGetValue(CurrentFormViewDataKey, out currentForm)) + throw new InvalidOperationException("Not currently inside a form section."); + + return (currentForm as IForm).CreatePartialForm(partial.PartialModelExpression(), partial.Html); + } + + /// + /// Whether or not a partial view is within a form section. + /// + /// View model of the partial view + /// View page for partial view + /// Whether the view is within a form section + public static bool IsInFormSection(this WebViewPage partial) + { + object currentSection; + return partial.ViewData.TryGetValue(CurrentFormSectionViewDataKey, out currentSection); + } + } +}