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
+
+
\ 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(@".*?(
@@ -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);
+ }
+ }
+}