Avoiding Repetitive View Code in ASP.NET MVC - asp.net-mvc-4

This is for an ASP.NET MVC 4 application in a Razor view. I am passing a model to a partial view and am trying to iterate through a given list of properties contained in the model to be displayed as a table.
Given something like List<string> propertyNames, in this table, I would like to output the DisplayNameFor and the value of the property in a structure like so:
<tr>
<th>#Html.DisplayNameFor(Model.property)</th>
<td>#Model.property</td>
</tr>
I'll have to do this a few times in the partial because different properties correspond to different div elements in the partial where different tables will be inserted so I've created a helper and this is where I get hung up. First, the only way I know how to do this is reflection, and I have read that reflection is expensive especially for just doing one property at a time. Also, using this method, I can't get #Html.DisplayNameFor to work correctly because, using reflection, I don't quite know the syntax:
#helper IterateDetailPropertyNames(List<string> propertyNames) {
foreach (var property in propertyNames)
{
<tr>
<th>
#Html.DisplayNameFor(m => m.GetType().GetProperty(property).GetValue(Model, null));
#*Error: Templates can be used only with field access, property access...*#
</th>
<td>
#Model.GetType().GetProperty(property).GetValue(Model, null)
</td>
</tr>
}
}
How can I make this work and improve this?

A models ModelMetadata gives you the data you need to generate your table. It contains the model value in additional to properties such as the DisplayName, DisplayFormatString, NullDisplayText etc that are determined from annotations applied to your properties.
For more information of ModelMetadata, refer the documentation and ASP.NET MVC 2 Templates, Part 2: ModelMetadata.
To generate your table, you could write the following HtmlHelper extension method
public static class TableHelper
{
public static MvcHtmlString DisplayTableFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression)
{
// Get the ModelMetadata for the model
ModelMetadata metaData = ModelMetadata.FromLambdaExpression(expression, helper.ViewData);
StringBuilder html = new StringBuilder();
// Loop through the ModelMetadata of each property in the model
foreach (ModelMetadata property in metaData.Properties)
{
var x = metaData.DisplayFormatString;
string label = property.DisplayName;
string value = null;
if (property.Model == null)
{
value = metaData.NullDisplayText;
}
else if (metaData.DisplayFormatString != null)
{
value = string.Format(metaData.DisplayFormatString, property.Model);
}
else
{
value = property.Model.ToString();
}
TagBuilder labelCell = new TagBuilder("td");
labelCell.InnerHtml = label;
TagBuilder valueCell = new TagBuilder("td");
valueCell.InnerHtml = value;
StringBuilder innerHtml = new StringBuilder();
innerHtml.Append(labelCell.ToString());
innerHtml.Append(valueCell.ToString());
TagBuilder row = new TagBuilder("tr");
row.InnerHtml = innerHtml.ToString();
html.Append(row.ToString());
}
TagBuilder table = new TagBuilder("table");
table.InnerHtml = html.ToString();
return new MvcHtmlString(table.ToString());
}
}
and in the view
#using YourAssembly.TableHelper
....
#Html.DisplayTableFor(m => m)
You can also include the assembly in your web.config file so that the #using statement is not required in the view
As s side note, <table> elements are for tabular data and should not be used for layout. Instead consider using styled <div> elements

Related

Validation messages from custom model validation attributes are locked to first loaded language

I am working on a multi lingual website using Umbraco 7.2.4 (.NET MVC 4.5). I have pages for each language nested under home nodes with their own culture:
Home (language selection)
nl-BE
some page
some other page
my form page
fr-BE
some page
some other page
my form page
The form model is decorated with validation attributes that I needed to translate for each language. I found a Github project, Umbraco Validation Attributes that extends decoration attributes to retrieve validation messages from Umbraco dictionary items. It works fine for page content but not validation messages.
The issue
land on nl-BE/form
field labels are shown in dutch (nl-BE)
submit invalid form
validation messages are shown in dutch (nl-BE culture)
browse to fr-BE/form
field labels are shown in french (fr-BE)
submit invalid form
Expected behavior is: validation messages are shown in french (fr-BE culture)
Actual behavior is: messages are still shown in dutch (data-val-required attribute is in dutch in the source of the page)
Investigation to date
This is not a browser cache issue, it is reproducible across separate browsers, even separate computers: whoever is generating the form for the first time will lock the validation message culture. The only way to change the language of the validation messages is to recycle the Application Pool.
I doubt that the Umbraco Validation helper class is the issue here but I'm out of ideas, so any insight is appreciated.
Source code
Model
public class MyFormViewModel : RenderModel
{
public class PersonalDetails
{
[UmbracoDisplayName("FORMS_FIRST_NAME")]
[UmbracoRequired("FORMS_FIELD_REQUIRED_ERROR")]
public String FirstName { get; set; }
}
}
View
#inherits Umbraco.Web.Mvc.UmbracoTemplatePage
var model = new MyFormViewModel();
using (Html.BeginUmbracoForm<MyFormController>("SubmitMyForm", null, new {id = "my-form"}))
{
<h3>#LanguageHelper.GetDictionaryItem("FORMS_HEADER_PERSONAL_DETAILS")</h3>
<div class="field-wrapper">
#Html.LabelFor(m => model.PersonalDetails.FirstName)
<div class="input-wrapper">
#Html.TextBoxFor(m => model.PersonalDetails.FirstName)
#Html.ValidationMessageFor(m => model.PersonalDetails.FirstName)
</div>
</div>
note: I have used the native MVC Html.BeginForm method as well, same results.
Controller
public ActionResult SubmitFranchiseApplication(FranchiseFormViewModel viewModel)
{
if (!ModelState.IsValid)
{
TempData["Message"] = LanguageHelper.GetDictionaryItem("FORMS_VALIDATION_FAILED_MESSAGE");
foreach (ModelState modelState in ViewData.ModelState.Values)
{
foreach (ModelError error in modelState.Errors)
{
TempData["Message"] += "<br/>" + error.ErrorMessage;
}
}
return RedirectToCurrentUmbracoPage();
}
}
LanguageHelper
public class LanguageHelper
{
public static string CurrentCulture
{
get
{
return UmbracoContext.Current.PublishedContentRequest.Culture.ToString();
// I also tried using the thread culture
return System.Threading.Thread.CurrentThread.CurrentCulture.ToString();
}
}
public static string GetDictionaryItem(string key)
{
var value = library.GetDictionaryItem(key);
return string.IsNullOrEmpty(value) ? key : value;
}
}
So I finally found a workaround. In attempt to reduce my app to its simplest form and debug it, I ended up recreating the "UmbracoRequired" decoration attribute. The issue appeared when ErrorMessage was set in the Constructor rather than in the GetValidationRules method. It seems that MVC is caching the result of the constructor rather than invoking it again every time the form is loaded. Adding a dynamic property to the UmbracoRequired class for ErrorMessage also works.
Here's how my custom class looks like in the end.
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter,
AllowMultiple = false)]
internal class LocalisedRequiredAttribute : RequiredAttribute, IClientValidatable
{
private string _dictionaryKey;
public LocalisedRequiredAttribute(string dictionaryKey)
{
_dictionaryKey = dictionaryKey;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
ModelMetadata metadata, ControllerContext context)
{
ErrorMessage = LanguageHelper.GetDictionaryItem(_dictionaryKey); // this needs to be set here in order to refresh the translation every time
yield return new ModelClientValidationRule
{
ErrorMessage = this.ErrorMessage, // if you invoke the LanguageHelper here, the result gets cached and you're locked to the current language
ValidationType = "required"
};
}
}

How to prevent inserting null value in Image field when the record is modified and saved using MVC4?

I am storing image in Database successfully.
I want to display this image in Edit form to modify and save changes. But I'm just able to display image in Edit form my code for modifying and save images in data base is not working it's inserting null values in Image field when the record is modified and saved or if I modify all other fields in Edit view excepting image field.
Would someone please tell me what mistake I'm doing? Here is my controller action:
public ActionResult Edit(student st) {
if (ModelState.IsValid) {
var imgFile = Request.Files["imgFile"];
if (imgFile != null && imgFile.ContentLength > 0) {
var fileName = Path.GetFileName(imgFile.FileName);
var path = Path.Combine(Server.MapPath("~/Content/stImgs"), fileName);
imgFile.SaveAs(path);
st.Img = fileName;
}
}
try {
db.Entry(st).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("student");
} catch {
return View(st);
}
}
Here is view:
<img src="/Content/Imgs/#Model.Img">
</div>
<label for="file">Image:</label>
<input type="file" name="file" id="file" />
#Html.ValidationMessageFor(item => item.file)
try adding a HttpPostedFileBase parameter in your action method:
public ActionResult Edit(student st, HttpPostedFileBase file)
{
if (ModelState.IsValid) {
if (file != null && file.ContentLength > 0) {
var fileName = Path.GetFileName(file.FileName);
var path = Path.Combine(Server.MapPath("~/Content/stImgs"), fileName);
file.SaveAs(path);
st.Img = fileName;
}
}
try {
db.Entry(st).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("student");
} catch {
return View(st);
}
}
Make sure the parameter's name is the same as the file's name in the form!
Also, make sure your Form is set to enctype multipart/form-data:
Html.BeginForm(
action, controller, FormMethod.Post, new { enctype="multipart/form-data"})
Microsoft change how update action method work.
please read section "Update the Edit HttpPost Method " from the following link.
These changes implement a security best practice to prevent overposting, The scaffolder generated a Bind attribute and added the entity created by the model binder to the entity set with a Modified flag. That code is no longer recommended because the Bind attribute clears out any pre-existing data in fields not listed in the Include parameter. In the future, the MVC controller scaffolder will be updated so that it doesn't generate Bind attributes for Edit methods.
The new code reads the existing entity and calls TryUpdateModel to update fields from user input in the posted form data. The Entity Framework's automatic change tracking sets the Modified flag on the entity. When the SaveChanges method is called, the Modified flag causes the Entity Framework to create SQL statements to update the database row. Concurrency conflicts are ignored, and all columns of the database row are updated, including those that the user didn't change. (A later tutorial shows how to handle concurrency conflicts, and if you only want individual fields to be updated in the database, you can set the entity to Unchanged and set individual fields to Modified.)

Variable number of HttpPostedFileBase objects

Currently, I have this:
public ActionResult Add(FormCollection form, HttpPostedFileBase fr, HttpPostedFileBase en, HttpPostedFileBase es)
{
Upload(fr, "fr");
Upload(en, "en");
Upload(es, "es");
...
}
This works for what we're doing currently, but just learned of a new requirement where the system needs the ability to add other languages. This is the only part where I have an issue.
I tried:
public ActionResult Add(FormCollection form, HttpPostedFileBase[] fr)
{
foreach(var file in fr)
{
Upload(file, "I'mStuck");
}
...
}
as a test, but it will only have 1 element and it is the one where id/name = fr. Makes sense, but not particularly helpful for what I need.
I could do:
for (string file in Request.Files)
{
...
}
which would handle the upload component fine, but the issue is that unless I can force them to standardize against a whatever_langabbreviation.extension file format, which I can't, I'm not going to be able to know what the language abbreviation is.
How can I obtain the id/name fields for the input type=file objects within the controller?
I was actually incorrect. The string returned is actually the id or name (think name, but considering I typically pair id/name, it works).
For the controller that renders the view initially, I did:
List<Languages> langs = db.Languages.ToList();
viewmodel.Languages = langs;
return View(viewmodel);
In the view itself:
foreach(Language lang in Model.Languages)
{
// Label
<input type="file" id="#lang.Abbreviation" name="#lang.Abbreviation" />
}
And in the post event:
foreach(string file in Request.Files)
{
HttpPostedFileBase fb = Request.Files[file];
Upload(fb, file);
}
And it handles as it is supposed to (Upload being a function that just adds a new item to another table.

Get all SmartForm items from Ektron 9 in a Taxonomy

I'm using Ektron CMS version 9.0
I have smart form content which is allocated to taxonomies e.g. I might have five smart form content items (all of same) type allocated to a taxonomy, and another three to a different taxonomy:
I need to get all content of a smart form type from a taxonomy:
public IEnumerable<T> GetListOfSmartFormFromTaxonomy<T>(long taxonomyId, bool isRecursive) where T : class
{
// TODO
}
What I have working, based on links below, is this:
public IEnumerable<TaxonomyItemData> GetListOfSmartFormFromTaxonomy(long taxonomyId)
{
TaxonomyItemCriteria criteria = new TaxonomyItemCriteria();
criteria.AddFilter(TaxonomyItemProperty.TaxonomyId, CriteriaFilterOperator.EqualTo, taxonomyId);
TaxonomyItemManager taxonomyItemManager = new TaxonomyItemManager();
List<TaxonomyItemData> taxonomyItemList = taxonomyItemManager.GetList(criteria);
return taxonomyItemList;
}
But this just gets the item's titles and ids, not the smart form data itself.
As an Ektron newbie, I don't know how to get all the items of one Smart Form type using only one call (instead of looping through each item and fetching it by ID which is not efficient)
What have I missed? I am working on this actively today and will post my findings here.
References used so far:
http://reference.ektron.com/developer/framework/Organization/TaxonomyItemManager/GetList.asp
Ektron taxonomy and library items (in v9)
EDIT
Posted my just-got-it-working solution below as an fyi and awarded closest answer as accepted. Thanks everyone for your help. Please chime in with any improvements ;)
I'd recommend using the ContentTaxonomyCriteria with the ContentManager.
long smartFormId = 42;
long taxonomyId = 127;
bool isRecursive = true;
var cm = new ContentManager();
var taxonomyCriteria = new ContentTaxonomyCriteria();
taxonomyCriteria.AddFilter(ContentProperty.XmlConfigurationId, CriteriaFilterOperator.EqualTo, smartFormId);
taxonomyCriteria.AddFilter(taxonomyId, isRecursive);
var content = cm.GetList(taxonomyCriteria);
UPDATE
The ContentData object has a property called XmlConfiguration. When the content is based on a smartform, this property will be non-null and have a positive (non-zero) Id: content[0].XmlConfiguration.Id for example.
I often add an Extension Method to my code that will tell me whether a given ContentData is based on a smart form:
public static class ContentDataExtensions
{
public static bool IsSmartFormContent(this ContentData content)
{
return content != null && content.XmlConfiguration != null && content.XmlConfiguration.Id > 0;
}
}
That way I can take a content (or list of content) and check it very quickly in code to see if it's based on a smartform or not:
foreach (var contentData in contentList)
{
if (contentData.IsSmartFormContent())
{
// Do smart-form stuff here...
}
}
Of course, if your content is coming from the framework api and you used a criteria object that is selecting based on a specific XmlConfigurationId, then in theory you wouldn't have to use that, but it still comes in handy quite often.
I'm not quite sure I understand your organizational structure, but you do have the ability to do your own sub clauses that select directly against the database.
In this case I wouldn't use the TaxonomyItemManager, I would use the ContentManager with a special criteria:
ContentManager cApi = new ContentManager();
var criteria = new ContentCriteria();
criteria.AddFilter(ContentProperty.Id, CriteriaFilterOperator.InSubClause, "select taxonomy_item_id where taxonomy_id = " + taxonomyId);
criteria.AddFilter(ContentProperty.XmlConfigurationId, CriteriaFilterOperator.EqualTo, smartformTypeId);
var yourContent = cApi.GetList(criteria);
That should do what you're asking for (grab the content specifically that is a member of a Taxonomy while only being of a specific SmartForm config). It's worth noting you don't need the second criteria piece (XmlConfigurationId) if your Taxonomy only contains that XmlConfiguration.
For Information, this is what I came up with. Noted Brian Oliver's comment on List but using patterns from other devs, can refactor later.
To clarify, we are creating classes from the XSDs generated from the smart forms, so have smart form types to play with. Your use may be simpler that ours.
public IEnumerable<T> GetListOfSmartFormFromTaxonomy<T>(long taxonomyId, bool isRecursive = false) where T : class
{
long smartFormId = GetSmartFormIdFromType(typeof(T));
// checks here for smartformid=0
ContentManager contentManager = new ContentManager();
ContentTaxonomyCriteria criteria = new ContentTaxonomyCriteria();
// Smart Form Type
criteria.AddFilter(ContentProperty.XmlConfigurationId, CriteriaFilterOperator.EqualTo, smartFormId);
// Taxonomy
criteria.AddFilter(taxonomyId, isRecursive);
List<ContentData> contentDataList = contentManager.GetList(criteria);
IEnumerable<T> smartFormList = ConvertToSmartFormList<T>(pressReleaseDataList);
return smartFormList;
}
private IEnumerable<T> ConvertToSmartFormList<T>(List<ContentData> contentDataList) where T : class
{
List<T> smartFormList = new List<T>();
if (contentDataList != null && contentDataList.Count > 0)
{
foreach (ContentData contentData in contentDataList)
{
if (contentData.IsSmartFormContent())
{
T smartForm = GetDeserializedContent<T>(contentData.Html);
if (smartForm != null)
{
PropertyInfo property = smartForm.GetType().GetProperty("ContentId");
if (property != null)
{
property.SetValue(smartForm, contentData.Id, null);
}
smartFormList.Add(smartForm);
}
}
}
}
return smartFormList;
}
private long GetSmartFormIdFromType(Type smartFormType)
{
SmartFormConfigurationManager manager = new SmartFormConfigurationManager();
SmartFormConfigurationCriteria criteria = new SmartFormConfigurationCriteria();
// Note: Smart Form Title must match the type's name, i.e. no spaces, for this to work
criteria.AddFilter(SmartFormConfigurationProperty.Title, CriteriaFilterOperator.EqualTo, smartFormType.Name);
List<SmartFormConfigurationData> configurationData = manager.GetList(criteria);
if (configurationData == null || configurationData.Count == 0)
{
return 0;
}
return configurationData.First().Id;
}

Add all query strings on controller action

I have a simple mvc4 application. An action link opens a view with bunch of query string parameters. The view contains a simple form when you click on submit button it posts the form and comes back to the view but I lost the query strings. what I must to do to have same query strings even after you have submitted the form?
One possibility is to specify the current request as action attribute of your form:
<form action="#Request.Url.AbsoluteUri" method="post">
...
</form>
But this will POST to the same resource. If you want to specify a different controller and/or action you could write a custom BeginForm helper which will do the job.
Something along the lines of:
public static class FormExtensions
{
public static IDisposable MyBeginForm(this HtmlHelper html, string controller, string action)
{
var builder = new TagBuilder("form");
var urlHelper = new UrlHelper(html.ViewContext.RequestContext);
var routeValues = new RouteValueDictionary();
var query = html.ViewContext.RequestContext.HttpContext.Request.QueryString;
foreach (string key in query)
{
routeValues[key] = query[key];
}
builder.MergeAttribute("action", urlHelper.Action(action, controller, routeValues));
builder.MergeAttribute("method", "POST", true);
html.ViewContext.Writer.Write(builder.ToString(TagRenderMode.StartTag));
var form = new MvcForm(html.ViewContext);
return form;
}
}
and then:
#using (Html.MyBeginForm("myaction", "mycontroller"))
{
...
}
This will effectively keep the current url query string parameters.