Generically Render Partial View - Convert PropertyInfo into Concrete Object - c#

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

Related

Can I tell MVC to separate pascal-cased properties in to words automatically?

I often have C# code like
[DisplayName("Number of Questions")]
public int NumberOfQuestions { get; set; }
Where I use the DisplayName property to add in spaces when it is displayed. Is there an option to tell MVC to add spaces by default if the DisplayName annotation is not explicitly provided?
Thanks
There are two ways.
1.Override LabelFor and creating a custom HTML helper:
Utility class for Custom HTML helper:
public class CustomHTMLHelperUtilities
{
// Method to Get the Property Name
internal static string PropertyName<T, TResult>(Expression<Func<T, TResult>> expression)
{
switch (expression.Body.NodeType)
{
case ExpressionType.MemberAccess:
var memberExpression = expression.Body as MemberExpression;
return memberExpression.Member.Name;
default:
return string.Empty;
}
}
// Method to split the camel case
internal static string SplitCamelCase(string camelCaseString)
{
string output = System.Text.RegularExpressions.Regex.Replace(
camelCaseString,
"([A-Z])",
" $1",
RegexOptions.Compiled).Trim();
return output;
}
}
Custom Helper:
public static class LabelHelpers
{
public static MvcHtmlString LabelForCamelCase<T, TResult>(this HtmlHelper<T> helper, Expression<Func<T, TResult>> expression, object htmlAttributes = null)
{
string propertyName = CustomHTMLHelperUtilities.PropertyName(expression);
string labelValue = CustomHTMLHelperUtilities.SplitCamelCase(propertyName);
#region Html attributes creation
var builder = new TagBuilder("label ");
builder.Attributes.Add("text", labelValue);
builder.Attributes.Add("for", propertyName);
#endregion
#region additional html attributes
if (htmlAttributes != null)
{
var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
builder.MergeAttributes(attributes);
}
#endregion
MvcHtmlString retHtml = new MvcHtmlString(builder.ToString(TagRenderMode.SelfClosing));
return retHtml;
}
}
Use in CSHTML:
#Html.LabelForCamelCase(m=>m.YourPropertyName, new { style="color:red"})
Your label will display as 'Your Property Name'
2.Using Resource File:
[Display(Name = "PropertyKeyAsperResourceFile", ResourceType = typeof(ResourceFileName))]
public string myProperty { get; set; }
I will prefer the first solution. Because a resource file is intend to do a separate and reserved role in a project. Additionally the custom HTML helper can be reused once created.

How to use TextBoxFor in helper?

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" }

Getting the owning object of a property from a Property Expression

I'm working on a bit of code which has an end purpose of letting you use a property expression to set the value of a property with similar syntax to passing a variable as an out or ref parameter.
Something along the lines of:
public static foo(()=>Object.property, value);
And Object.Property will be assigned the value of value.
I'm using the following code to get the owining object of the property:
public static object GetOwningObject<T>(this Expression<Func<T>> #this)
{
var memberExpression = #this.Body as MemberExpression;
if (memberExpression != null)
{
var fieldExpression = memberExpression.Expression as MemberExpression;
if (fieldExpression != null)
{
var constExpression = fieldExpression.Expression as ConstantExpression;
var field = fieldExpression.Member as FieldInfo;
if (constExpression != null) if (field != null) return field.GetValue(constExpression.Value);
}
}
return null;
}
So this would, when used on a property expression like ()=>Object.Property, give back the instance of 'Object'. I'm somewhat new to using property expressions, and there seems to be many different ways to accomplish things, but I want to extend what I have so far, so that given an expression such as ()=>Foo.Bar.Baz it will give the Bar, not Foo. I always want the last containing object in the expression.
Any ideas? Thanks in advance.
What you have to do is to traverse through property chain to the most outer object.
The sample below is rather self explanatory and shows that the extension method would work for chained fields as well as properties:
class Foo
{
public Bar Bar { get; set; }
}
class Bar
{
public string Baz { get; set; }
}
class FooWithField
{
public BarWithField BarField;
}
class BarWithField
{
public string BazField;
}
public static class LambdaExtensions
{
public static object GetRootObject<T>(this Expression<Func<T>> expression)
{
var propertyAccessExpression = expression.Body as MemberExpression;
if (propertyAccessExpression == null)
return null;
//go up through property/field chain
while (propertyAccessExpression.Expression is MemberExpression)
propertyAccessExpression = (MemberExpression)propertyAccessExpression.Expression;
//the last expression suppose to be a constant expression referring to captured variable ...
var rootObjectConstantExpression = propertyAccessExpression.Expression as ConstantExpression;
if (rootObjectConstantExpression == null)
return null;
//... which is stored in a field of generated class that holds all captured variables.
var fieldInfo = propertyAccessExpression.Member as FieldInfo;
if (fieldInfo != null)
return fieldInfo.GetValue(rootObjectConstantExpression.Value);
return null;
}
}
[TestFixture]
public class Program
{
[Test]
public void Should_find_root_element_by_property_chain()
{
var foo = new Foo { Bar = new Bar { Baz = "text" } };
Expression<Func<string>> expression = () => foo.Bar.Baz;
Assert.That(expression.GetRootObject(), Is.SameAs(foo));
}
[Test]
public void Should_find_root_element_by_field_chain()
{
var foo = new FooWithField { BarField = new BarWithField { BazField = "text" } };
Expression<Func<string>> expression = () => foo.BarField.BazField;
Assert.That(expression.GetRootObject(), Is.SameAs(foo));
}
}
If your project is an MVC 5 project and you have a reference to the assembly System.Web.Mvc You could use the following:
Some time ago I wrote an extension method to easily create a multiselect dropdown (based on bootstrap 4) and it looked something like this:
public static MvcHtmlString MultiSelectFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
{
/*The challenge I faced here was that the expression you passed could very well be nested, so in order overcome this, I decompiled the dll to see how MVC does it, and I found this piece of code.*/
string expressionText = System.Web.Mvc.ExpressionHelper.GetExpressionText((LambdaExpression)expression);
System.Web.Mvc.ModelMetadata metadata = System.Web.Mvc.ModelMetadata.FromStringExpression(expressionText, htmlHelper.ViewData);
}
The metadata object has a Property called PropertyName and another property called Container which is a reference to the instance of the container object.

How to manipulate values of a tag?

This is different approach to translation of page. It is not meant to be localization.
I created class TransModel which all my ViewModels are inheriting from.
This class fetches string pairs relevant for current ViewModel from database and stores them in "labels" Dictionary. Key for that pair is the value of string here "User Name" and value is translated value.
[Display(Name = "User Name")]
public string UserName { get; set; }
Instead of using Html.LabelFor in View I use extension of it call it TransLabelFor
public static MvcHtmlString TransLabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression,TransModel model)
{
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
string trans = "";
model.labels.TryGetValue(metadata.DisplayName, out trans);
if (trans == null)
{
trans = metadata.DisplayName;
}
return MvcHtmlString.Create(String.Format("<label for='{0}'>{1}</label>", metadata.DisplayName, trans));
}
Now I want to replace what I return by it. I want the original tag as returned by this:
MvcHtmlString originalTag = System.Web.Mvc.Html.LabelExtensions.LabelFor(html, expression);
but with my translation.
Are there any neat ways of doing it instead of string find/replace? I don't like that I have to pass Model around either, any better ideas?
Or "improvement" to your solution (also including reflection, but this will avoid to put TransModel in your helpers)
public static IHtmlString TransLabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
var htmlFieldName = ExpressionHelper.GetExpressionText(expression);
var modelType = typeof (TModel);
var resolvedLabelText = metadata.DisplayName ?? metadata.PropertyName;
if (String.IsNullOrEmpty(resolvedLabelText))
return MvcHtmlString.Empty;
var labelProperty = modelType.GetProperty("labels");
if (labelProperty != null)
{
var labels = labelProperty.GetValue(html.ViewData.Model, null) as Dictionary<string, string>;
if (labels != null && labels.ContainsKey(resolvedLabelText))
resolvedLabelText = labels[resolvedLabelText];
}
var tag = new TagBuilder("label");
tag.Attributes.Add("for", TagBuilder.CreateSanitizedId(html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName)));
tag.SetInnerText(resolvedLabelText);
return MvcHtmlString.Create(tag.ToString());
}
but this will mean you'll have to change all your LabelFor to TransLableFor (string replace in all solution) : but at least you don't have to add a "TransModel" during the replacement.

TryUpdateModel Not Working with Prefix and IncludeProperties

Hi i have an entity called User with 2 properties called UserName and Role (which is a reference to another entity called Role). I'm trying to update the UserName and RoleID from a form which is posted back. Within my postback action i have the following code:
var user = new User();
TryUpdateModel(user, "User", new string[] { "UserName, Role.RoleID" });
TryUpdateModel(user, new string[] { "User.UserName, User.Role.RoleID" });
However none of these updates the Role.RoleID property. If i try the following:
TryUpdateModel(user, "User", new string[] { "UserName, Role" });
TryUpdateModel(user);
The RoleID is updated but the RoleName property is validated aswell. That's why i'm trying to be more specific on which properties to update but i can't get any of the first examples to work.
I'd appreciate if someone could help. Thanks
Here's a complete solution to work with any relationship.
First place the following line of code in your Application_Start event:
ModelBinders.Binders.DefaultBinder = new CustomModelBinder();
Now you need to add the following class somewhere in your application:
public class CustomModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (bindingContext.ModelType.Namespace.EndsWith("Models.Entities") && !bindingContext.ModelType.IsEnum && value != null)
{
if (Utilities.IsInteger(value.AttemptedValue))
{
var repository = ServiceLocator.Current.GetInstance(typeof(IRepository<>).MakeGenericType(bindingContext.ModelType));
return repository.GetType().InvokeMember("GetByID", BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public, null, repository, new object[] { Convert.ToInt32(value.AttemptedValue) });
}
else if (value.AttemptedValue == "")
return null;
}
return base.BindModel(controllerContext, bindingContext);
}
}
Please note the above code may need to be modified to suit your needs. It affectively calls IRepository().GetByID(???). It will work when binding against any entities within the Models.Entities namespace and that have an integer value.
Now for the view there's one other bit of work you have to do to fix a bug in ASP.NET MVC 2. By default the Selected property on a SelectListItem is ignored so i have come up with my own DropDownListFor which allows you to pass in the selected value.
public static class SelectExtensions
{
public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string selectedValue, string optionLabel)
{
return DropDownListFor(helper, expression, selectList, selectedValue, optionLabel, null);
}
public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string selectedValue, string optionLabel, object htmlAttributes)
{
return DropDownListHelper(helper, ExpressionHelper.GetExpressionText(expression), selectList, selectedValue, optionLabel, new RouteValueDictionary(htmlAttributes));
}
/// <summary>
/// This is almost identical to the one in ASP.NET MVC 2 however it removes the default values stuff so that the Selected property of the SelectListItem class actually works
/// </summary>
private static MvcHtmlString DropDownListHelper(HtmlHelper helper, string name, IEnumerable<SelectListItem> selectList, string selectedValue, string optionLabel, IDictionary<string, object> htmlAttributes)
{
name = helper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
// Convert each ListItem to an option tag
var listItemBuilder = new StringBuilder();
// Make optionLabel the first item that gets rendered
if (optionLabel != null)
listItemBuilder.AppendLine(ListItemToOption(new SelectListItem() { Text = optionLabel, Value = String.Empty, Selected = false }, selectedValue));
// Add the other options
foreach (var item in selectList)
{
listItemBuilder.AppendLine(ListItemToOption(item, selectedValue));
}
// Now add the select tag
var tag = new TagBuilder("select") { InnerHtml = listItemBuilder.ToString() };
tag.MergeAttributes(htmlAttributes);
tag.MergeAttribute("name", name, true);
tag.GenerateId(name);
// If there are any errors for a named field, we add the css attribute
ModelState modelState;
if (helper.ViewData.ModelState.TryGetValue(name, out modelState))
{
if (modelState.Errors.Count > 0)
tag.AddCssClass(HtmlHelper.ValidationInputCssClassName);
}
return tag.ToMvcHtmlString(TagRenderMode.Normal);
}
internal static string ListItemToOption(SelectListItem item, string selectedValue)
{
var tag = new TagBuilder("option") { InnerHtml = HttpUtility.HtmlEncode(item.Text) };
if (item.Value != null)
tag.Attributes["value"] = item.Value;
if ((!string.IsNullOrEmpty(selectedValue) && item.Value == selectedValue) || item.Selected)
tag.Attributes["selected"] = "selected";
return tag.ToString(TagRenderMode.Normal);
}
}
Now within your view you can say:
<%= Html.DropDownListFor(m => m.User.Role, Model.Roles, Model.User.Role != null ? Model.User.Role.RoleID.ToString() : "", "-- Please Select --")%>
<%= Html.ValidationMessageFor(m => m.User.Role, "*")%>
The Role property will automatically update when you call TryUpdateModel in your controller and you have to do no additional work to wire this up. Although it's alot of code initially i've found this approach saves heaps of code in the long term.
Hope this helps.
Consider calling TryUpdateModel twice, once for the User. Once for the Role. Then assign the role to the user.
var user = new User();
var role = new Role();
TryUpdateModel(user, new string[] { "UserName" });
TryUpdateModel(role, new string[] { "RoleID" });
user.Role = role;
See if that works.

Categories

Resources