My goal is to create an object to allow chaining of commands in MVC.Net views.
Here is an example use in a view of a menu I created using this concept:
<nav class="navigation">
<%: Html
.menu()
.item("Introduction", "Introduction", "Home")
.item("About", "About", "Home")
.item("Systems", "Index", "Systems")
/*.item("Categories", "Categories", "Health")*/
.item("Test Cases", "TestCases", "Testing")
.category("Logging")
.item("UniMon Events", "UniMonEvents", "Logging")
.end()
.end() %>
</nav>
As you can see it allows for the quick construction of a multi-tiered menu with interdependencies between the various parts.
I would like to achieve this same effect for a form using lambda expressions.
The ideal syntax would look like this:
<%: Html
.form()
.hidden(m=>m.property1)
.hidden(m=>m.property2)
.end() %>
Where I am running into trouble is with the hidden method. It seems there is no way to get the compiler to infer m without passing it to the method hidden.
I can achieve this syntax:
<%: Html
.form()
.hidden(Html, m=>m.property1)
.hidden(Html, m=>m.property2)
.end() %>
Using this class and an extension method(not shown):
public class RouteForm
{
public HtmlHelper HtmlHelper { get; private set; }
public Dictionary<string, string> PostData { get; private set; }
public RouteForm(HtmlHelper htmlHelper)
{
HtmlHelper = htmlHelper;
PostData = new Dictionary<string, string>();
}
public RouteForm hidden<TModel, TValue>(HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression)
{
string name = ExpressionHelper.GetExpressionText(expression);
string value = GetFieldValue(htmlHelper, expression);
PostData.Add(name, value);
return this;
}
private static string GetFieldValue<TModel, TValue>(HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression)
{
object oValue = expression.Compile()(htmlHelper.ViewData.Model);
string value = (oValue is Enum) ? ((int)oValue).ToString() : oValue.ToString();
return value; ;
}
public MvcHtmlString end()
{
//TODO: render form with post data
return MvcHtmlString.Empty;
}
}
I thought that perhaps a class with a generic type might be what I am looking for, so I tried this:
public class RouteForm<TModel>
{
public HtmlHelper<TModel> HtmlHelper { get; private set; }
public Dictionary<string, string> PostData { get; private set; }
public RouteForm(HtmlHelper<TModel> htmlHelper)
{
HtmlHelper = htmlHelper;
PostData = new Dictionary<string, string>();
}
public RouteForm<TModel> hidden<TModel, TValue>(Expression<Func<TModel, TValue>> expression)
{
string name = ExpressionHelper.GetExpressionText(expression);
string value = GetFieldValue(expression);
PostData.Add(name, value);
return this;//ERRORS: TModel is TModel
}
private string GetFieldValue<TModel, TValue>(Expression<Func<TModel, TValue>> expression)
{
object oValue = expression.Compile()(
(TModel)HtmlHelper.ViewData.Model //ERRORS: Cannot convert type TModel to TModel
);
string value = (oValue is Enum) ? ((int)oValue).ToString() : oValue.ToString();
return value; ;
}
public MvcHtmlString end()
{
//TODO: render form with post data
return MvcHtmlString.Empty;
}
}
I put the errors in the code above using comments.
Thanks!
You're using too many generic parameters.
Methods like GetFieldValue<TModel, ...> create a second TModel paramter which is not related to the first one.
In other words, they allow you to write
new RouteForm<PersonModel>().GetFieldValue<TruckModel, ...>()
This is obviously wrong.
Instead, just get rid of that parameter from each method and let them use the class' TModel parameter instead.
I guess the compilation error "ERRORS: TModel is TModel" is caused by declaring TModel twice in the generic declaration of hidden().
I haven't compiled this, but I'd try something like this:
public static class HtmlHelperExtensions
{
public static RouteForm<TModel> form(this HtmlHelper helper, TModel model)
{
return new RouteForm<TModel>(helper);
}
}
public class RouteForm<TModel>
{
public RouteForm<TModel> hidden(Expression<Func<TModel, TValue>> expression)
{
}
public MvcHtmlString end()
{
}
}
Thanks to both of you I was able to create a class that achieves the syntax I was looking for.
(the class is simplified for this post)
class:
public class RouteForm<TModel>
{
public HtmlHelper<TModel> HtmlHelper { get; private set; }
public RouteForm(HtmlHelper<TModel> htmlHelper)
{
HtmlHelper = htmlHelper;
}
public RouteForm<TModel> hidden<TValue>(Expression<Func<TModel, TValue>> expression)
{
return this;
}
public MvcHtmlString end()
{
return MvcHtmlString.Empty;
}
}
extension method:
public static RouteForm<TModel> form<TModel>(this HtmlHelper<TModel> helper)
{
return new RouteForm<TModel>(helper);
}
markup syntax:
<%: Html
.form()
.hidden(m=>m.Property1)
.hidden(m=>m.Property2)
.end()
%>
Related
I am currently making a wizard in MVC (c#). But I have an if statement in my Wizard view that goes like this:
if (Model.Wizard.ClientDetails.GetStep() == Model.Wizard.CurrentStep)
{
#Html.PartialFor(x => x.Wizard.ClientDetails, "_Step");
}
else if (Model.Wizard.Preferences.GetStep() == Model.Wizard.CurrentStep)
{
#Html.PartialFor(x => x.Wizard.ClientPreferences, "_Step")
}
else if (Model.Wizard.ClientQuestions.GetStep() == Model.Wizard.CurrentStep)
{
#Html.PartialFor(x => x.Wizard.ClientQuestions, "_Step")
}
The wizards have been set up pretty generically except for this part of the view where I choose which partial to display. As you can see from the code above each if follows the same structure. The only part that changes is the Model.Wizard.**Property** part.
I wanted to try and remove this if statement so I don't have to worry about writing an if statement for each step I add to a new wizard.
I want to change the code to just something like this:
#Html.PartialFor(x => x.ExampleWizardTransaction.GetStepObject(), "_Step");
My current attempt for the GetStepObject method is as follows:
public static T GetStepObject<T>(this IWizardTransaction wizardTransaction)
where T : class, new()
{
var properties = wizardTransaction.GetType().GetProperties()
.Where(x => x.PropertyType.GetCustomAttributes(typeof(StepAttribute), true).Any());
PropertyInfo #object = properties.FirstOrDefault(x => ((StepAttribute)Attribute
.GetCustomAttribute(x.PropertyType, typeof(StepAttribute))).Step == wizardTransaction.CurrentStep);
}
The PropertyInfo #object part is correctly selecting the property info for the current step in the wizard. I need to be able to return the PropertyInfo #object PropertyInfo as its correct type with its current values and return it somehow.
Is this possible?
EDIT #1:
Existing PartialFor that works in normal scenarios.
public static MvcHtmlString PartialFor<TModel, TProperty>(
this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression, string partialViewName)
{
var name = ExpressionHelper.GetExpressionText(expression);
var model = ModelMetadata.FromLambdaExpression(expression, helper.ViewData).Model;
var viewData = new ViewDataDictionary(helper.ViewData)
{
TemplateInfo = new TemplateInfo { HtmlFieldPrefix = name }
};
return helper.Partial(partialViewName, model, viewData);
}
EDIT #2:
The reason the values are not getting binded is that the var name = ExpressionHelper.GetExpressionText(expression); part is returning a blank string. If I hard code the name variable to the actual property then the binding works. For example:
public static MvcHtmlString PartialFor<TModel, TProperty>(this HtmlHelper<TModel> helper,
Expression<Func<TModel, TProperty>> expression, string partialViewName)
{
var compiled = expression.Compile();
var result = compiled.Invoke(helper.ViewData.Model);
var name = ExpressionHelper.GetExpressionText(expression);
//Should be ExampleWizardTransaction.ClientDetails for this step but is blank
var viewData = new ViewDataDictionary(helper.ViewData)
{
TemplateInfo = new TemplateInfo
{
//HtmlFieldPrefix = name
HtmlFieldPrefix = "ExampleWizardTransaction.ClientDetails"
}
//Hard coded this to ExampleWizardTransaction.ClientDetails and the bindings now work
};
return helper.Partial(partialViewName, result, viewData);
}
It seems I need to be able to get the name of the wizard object and the current step object as a string value to pass into TemplateInfo.
I'm gonna take a wild guess at your class structures. Assuming your classes are something like this:
[AttributeUsage(AttributeTargets.Property, AllowMultiple =false)]
public class StepAttribute: Attribute
{
public StepEnum Step { get; set; }
}
public interface IWizardStep
{
}
public interface IWizardTransaction
{
}
public enum StepEnum
{
Previous,
CurrentStep
}
public class WizardStep: IWizardStep
{
public string StepName { get; set; }
public override string ToString()
{
return StepName;
}
}
public class Wizard : IWizardTransaction
{
[Step(Step = StepEnum.Previous)]
public WizardStep ClientDetails => new WizardStep() { StepName = "ClientDetails" };
[Step(Step = StepEnum.CurrentStep)]
public WizardStep ClientQuestions => new WizardStep() { StepName = "ClientQuestions" };
}
Assuming also this implementation of PartialFor method
public static MvcHtmlString PartialFor<TModel, TProperty>(this HtmlHelper<TModel> html,
Expression<Func<TModel, TProperty>> expression, string partialViewName)
{
var compiled = expression.Compile();
var result = compiled.Invoke(html.ViewData.Model);
return html.Partial(partialViewName, result);
}
Then this implementation of GetStepObject will work
public static TProperty GetStepObject<TProperty>(this IWizardTransaction wizardTransaction)
where TProperty : class
{
var properties = wizardTransaction.GetType().GetProperties()
.Where(x => x.GetCustomAttributes(typeof(StepAttribute), true).Any());
PropertyInfo #object = properties.FirstOrDefault(x =>
(x.GetCustomAttributes(typeof(StepAttribute), true).SingleOrDefault()
as StepAttribute).Step == StepEnum.CurrentStep);
return #object.GetValue(wizardTransaction) as TProperty;
}
With this implementation of a partial view named _Step.cshtml like this
#model PartialView.Models.WizardStep
#Model
Your view can call it like this
#model PartialView.Models.Wizard
#using PartialView.Models;
#{
ViewBag.Title = "Partial view calling";
}
#Html.PartialFor(m=>m.GetStepObject<WizardStep>(), "_Step")
And the visual result will be a blank page with the html text ClientQuestions
Since #Html.EditorFor() is building an input field according to the model data type. I'd like to use this method and generate a textbox for a model variable of the DATETIME type, instead of a calendar. Is there any way of doing this?
You can create your own helper to display and manage date.
public static MvcHtmlString DatePickerFor<TModel, TProperty>
(this HtmlHelper<TModel> helper,
Expression<Func<TModel, TProperty>> expression)
{
string datePickerName =
ExpressionHelper.GetExpressionText(expression);
string datePickerFullName = helper.ViewContext.ViewData.
TemplateInfo.GetFullHtmlFieldName
(datePickerName);
string datePickerID = TagBuilder.CreateSanitizedId
(datePickerFullName);
ModelMetadata metadata = ModelMetadata.FromLambdaExpression
(expression, helper.ViewData);
DateTime datePickerValue = (metadata.Model == null ?
DateTime.Now : DateTime.Parse(
metadata.Model.ToString()));
TagBuilder tag = new TagBuilder("input");
tag.Attributes.Add("name", datePickerFullName);
tag.Attributes.Add("id", datePickerID);
tag.Attributes.Add("type", "date");
tag.Attributes.Add("value", datePickerValue.
ToString("yyyy-MM-dd"));
IDictionary<string, object> validationAttributes = helper.
GetUnobtrusiveValidationAttributes
(datePickerFullName, metadata);
foreach (string key in validationAttributes.Keys)
{
tag.Attributes.Add(key, validationAttributes[key].ToString());
}
MvcHtmlString html=new MvcHtmlString(
tag.ToString(TagRenderMode.SelfClosing));
return html;
}
to prepare the use of the helper
public class EmployeeMetadata
{
[Required]
public DateTime BirthDate { get; set; }
}
[MetadataType(typeof(EmployeeMetadata))]
public partial class Employee
{
}
public ActionResult Index()
{
NorthwindEntities db = new NorthwindEntities();
return View(db.Employees.First());
}
and you can use your helper
<script src="~/Scripts/jquery-1.10.2.js"></script>
<script src="~/Scripts/jquery.validate.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.js"></script>
#using(Html.BeginForm("ProcessForm","Home",FormMethod.Post))
{
#Html.DatePicker("birthdateUnbound")
#Html.DatePickerFor(m=>m.BirthDate)
#Html.ValidationMessageFor(m=>m.BirthDate)
<input type="submit" value="Submit" />
}
To submit your form
public ActionResult ProcessForm(Employee obj)
{
return View("Index",obj);
}
I've this code in razor page.
#{ var countryCode = Model.CountryCode;}
#if (countryCode.Equals("CA") || countryCode.Equals("US"))
{
#Html.DropDownListFor(m => m.ProvState, Model.Provinces)
}
else
{
#Html.TextBoxFor(m => m.ProvState, Model.ProvState);
}
I wrote this helper in LocatioHelper.cshtml
#helper RenderProvince(LocationModel model)
{
var countryCode = model.CountryCode;
if (countryCode.Equals("CA") || countryCode.Equals("US"))
{
#Html.DropDownListFor(model.ProvState, model.Provinces)
}
else
{
#Html.TextBoxFor(model.ProvState, model.ProvState);
}
}
I've error that I must specify type for dropDownListFor and TextBoxFor explicitly, but on view I don't specify type.
How to move this into helper ?
As per my opinion, you can pack your custom helper methods in a static class and declare your methods as simple extension methods on HtmlHelper<T>.
public static class CustomHelper
{
public static MvcHtmlString YourCustomHelper<TModel, TValue> (
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TValue>> expression ---- )
{
//Write your custom logic here.
}
}
You can import this class in your view and use this helper method using the usual syntax of #Html.YourCustomHelper()
Hope this helps.
EDIT:
In case you are using inline helper method keep the helper method in your View only where it is being used. Otherwise try creating an external html helper method as indicated above.
No, this is not possible. You could write a normal HTML helper with #Html.TextBoxFor because that your view is strongly typed.
So you need something like:
public class HelperExtentions
{
public static MvcHtmlString DefaultRenderer<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, LocationModel model, SelectList selectList, object htmlAttributes)
{
var sb = new StringBuilder();
var countryCode = string.IsNullOrEmpty(model.CountryCode):"":model.CountryCode;
var dtp = "";
if (countryCode.Equals("CA") || countryCode.Equals("US"))
{
dtp = htmlHelper.DropDownListFor(expression, selectList, htmlAttributes);
}
else
{
dtp = htmlHelper.TextBoxFor(expression, htmlAttributes).ToHtmlString();
}
sb.AppendFormat(dtp);
return MvcHtmlString.Create(sb.ToString());
}
}
Then you can use :
#html.DefaultRenderer((m => m.ProvState, Models, newSelectList(/*some list data*/) ,new { #class = "form-control" }
I need to get working validation of the custom ASP.NET MVC helper.
Helper
public static class AutocompleteHelper
{
public static MvcHtmlString AutocompleteFor<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, string actionUrl)
{
return CreateAutocomplete(helper, expression, actionUrl, null, null);
}
public static MvcHtmlString AutocompleteFor<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, string actionUrl, bool? isRequired, string placeholder)
{
return CreateAutocomplete(helper, expression, actionUrl, placeholder, isRequired);
}
private static MvcHtmlString CreateAutocomplete<TModel, TValue>(HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, string actionUrl, string placeholder, bool? isRequired)
{
var attributes = new Dictionary<string, object>
{
{ "data-autocomplete", true },
{ "data-action", actionUrl }
};
if (!string.IsNullOrWhiteSpace(placeholder))
{
attributes.Add("placeholder", placeholder);
}
if (isRequired.HasValue && isRequired.Value)
{
attributes.Add("required", "required");
}
attributes.Add("class", "form-control formControlAutocomplete");
attributes.Add("maxlength", "45");
Func<TModel, TValue> method = expression.Compile();
var value = method((TModel)helper.ViewData.Model);
var baseProperty = ((MemberExpression)expression.Body).Member.Name;
var hidden = helper.Hidden(baseProperty, value);
attributes.Add("data-value-name", baseProperty);
var automcompleteName = baseProperty + "_autocomplete";
var textBox = helper.TextBox(automcompleteName, null, string.Empty, attributes);
var builder = new StringBuilder();
builder.AppendLine(hidden.ToHtmlString());
builder.AppendLine(textBox.ToHtmlString());
return new MvcHtmlString(builder.ToString());
}
}
HTML
#Html.AutocompleteFor(x => x.ProductUID, Url.Action("AutocompleteProducts", "Requisition"), true, "Start typing Product name...")
#Html.ValidationMessageFor(x => x.ProductUID)
I seems like validating but no message appears.
Any clue?
The name of your text field is ProductUID_autocomplete but your ValidationMessageFor which is supposed to display the error message is bound to ProductUID.
So make sure that you are binding your error message to the same property:
#Html.ValidationMessage("ProductUID_autocomplete")
It appears that whatever custom logic you might have to validate this field is injecting the error under the ProductUID_autocomplete key in the ModelState.
This being said, why not just invoke the ValidationMessage helper inside your custom helper? This way you will have less things to type in your view and the logic with those names being suffixed with _autocomplete will stay inside the helper only.
I have this view model:
public class BankAccountViewModel : IValidator<BankAccountViewModel>
{
public BankAccount BankAccount { get; set; }
public void ConfigureValidation(ValidationConfiguration<StockOnHandViewModel> config)
{
config.For(m => m.BankAccount.AccountHolder);
}
}
so the class is:
public class BankAccount
{
public string AccountHolder { get; set; }
public List<Transfer> Transfers { get; set; }
}
public class Transfer
{
public int Amount { get; set; }
}
Then I have some extensions for validation
public static class ValidationExtensions
{
public static PropertyRuleSet<TModel, TProperty> For<TModel, TProperty>(
this ValidationConfiguration<TModel> validationConfiguration,
Expression<Func<TModel, TProperty>> accessor)
{
return validationConfiguration.Add(accessor);
}
}
So I was able to call the For method for AccountHolder config.For(m => m.BankAccount.AccountHolder);
So that's cool. It sends through the expression as expected.
It becomes a little more difficult for the list items Transfers.
In transfers I might want to send through the expression for the amount of each transfer. So if I had two transfers I would want to send:
m => m.BankAccount.Transfers[0].Amount
m => m.BankAccount.Transfers[1].Amount
I know I could do it this way:
for(int i=0; i < BankAccount.Transfers.Count; i++)
{
config.For(m => m.BankAccount.Transfers[i].Amount);
}
However I don't want to be doing this for list items all the time.
I really want to maybe have a different For method for list items that I could some how call and it would do this for me.
I was thinking maybe of something like:
public static PropertyRuleSet<TModel, TProperty> For<TModel, TProperty>(
this ValidationConfiguration<TModel> validationConfiguration,
Expression<Func<TModel, TProperty>> accessor, int count)
{
...
}
where you maybe call it like:
config.For(m => m.BankAccount.Transfers[i].Amount, BankAccount.Transfers.Count);
However this won't work because I'm not sure how to send the expression part without the i and then populate it for each list item later. Also not sure what to do in the method
Anyone know what to do here?
Having it return a list seems linqy:
public static PropertyRuleSet<TModel, TProperty> For<TModel, TProperty>(
this ValidationConfiguration<TModel> validationConfiguration,
Expression<Func<TModel,IEnumerator<TProperty>>> accessor)
{
...
}
and then this:
config.For(m => m.BankAccount.Transfers.Select(t => t.Amount));
I have not tested it so it might have a typo.