I'm trying to create form from such model:
class NewContractorModel
{
//...
public PhotoModel photos { get; set; }
//...
}
class PhotoModel
{
public List<Photo> f { get; set; }
}
From controller I do some manipulation (actually I removed some photos from the collection) on the model object and put them into the view page using this:
return new View("SomeView", model);
I've tried to create inputs (lets say hidden inputs) for each Photo.
for (int i = 0; i < Model.photos.f.Count; ++i)
{
#Html.HiddenFor(m => m.photos.f[i].Uri)
#Html.HiddenFor(m => m.photos.f[i].ThumbnailUri)
#Html.HiddenFor(m => m.photos.f[i].SmallThumbnailUri)
#Html.TextBoxFor(m => m.photos.f[i].Description, new { placeholder = "Dodaj opis" })
}
But as I noticed that this doesnt work because it dismiss all of model modifications (it still stores all Photos in List despite the fact that I've removed them in Controler method).
Then I tried this code:
for (int i = 0; i < Model.photos.f.Count; ++i)
{
Photo photo = Model.photos.f[i];
<input id="photos_f_#{#i}__Uri" name="photos.f[#{#i}].Uri" type="hidden" value="#photo.Uri"/>
<input id="photos_f_#{#i}__ThumbnailUri" name="photos.f[#{#i}].ThumbnailUri" type="hidden" value="#photo.ThumbnailUri"/>
<input id="photos_f_#{#i}__SmallThumbnailUri" name="photos.f[#{#i}].SmallThumbnailUri" type="hidden" value="#photo.SmallThumbnailUri"/>
<input id="photos_f_#{#i}__Description" name="photos.f[#{#i}].Description" placeholder="Dodaj opis" type="text" value="#photo.Description"/>
}
...and this time IT WORKS!
Can anyone explain me what is the difference between those two parts of code?
I've tried to swich this code more than ten times and it always work the same so it's not my fault. ;)
I think that there is a bug in HtmlHelper methods but is there any walk-around ? I'd like to use helpers methods instead of raw html.
EDIT:
This is simplified controller class.
public class MyController
{
private NewContractorModel _model = null;
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
_model = SerializationUtility.Deserialize(Request.Form["Data"]) as NewContractorModel;
if (_model == null)
_model = TempData["Data"] as NewContractorModel;
if (_model == null)
_model = new NewContractorModel() as NewContractorModel;
TryUpdateModel(_model);
}
protected override void OnResultExecuted(ResultExecutedContext filterContext)
{
if (filterContext.Result is RedirectToRouteResult)
TempData["Data"] = _model;
}
private bool CheckModel(object model)
{
Type type = model.GetType();
PropertyInfo[] properties = type.GetProperties();
foreach (PropertyInfo p in properties)
{
object[] attr = p.GetCustomAttributes(true);
foreach (object a in attr)
{
if (a is ValidationAttribute)
{
object value = p.GetValue(model, null);
if (!((ValidationAttribute)a).IsValid(value))
return false;
}
}
}
return true;
}
protected ActionResult SelectPage(string delPhoto)
{
if (!CheckModel(_model))
{
// Do some action
}
//.....
foreach (ZAY.Database.Photo p in _model.photos.f)
{
if (p.Uri == Request["delPhoto"])
{
_model.photos.f.Remove(p);
break;
}
}
//.....
return View("SomeView", _model);
}
}
I noticed that inside lambdas the model looks just like after TryUpdateModel call (before modifications). If I don't use lambdas the model is modified... :/
And also my Photo class (generated from EntityFramework - so there is nothing interesting) and also simplified:
public class Photo : EntityObject
{
[Required]
public string Uri { get; set; }
[Required]
public string ThumbnailUri { get; set; }
[Required]
public string SmallThumbnailUri { get; set; }
public string Description { get; set; }
}
I'm sorry that I'm writing only such small snippets but the whole code is more complicated - there is only the most interesting part of it.
This is the answer to my problem:
http://blogs.msdn.com/b/simonince/archive/2010/05/05/asp-net-mvc-s-html-helpers-render-the-wrong-value.aspx
I wonder why it is not mentioned in documentation... :/
From your description, I don't really understand what's going wrong in your first sample. But you certainly have a problem with the scope of the loop variable i.
Since the expression m => m.photos.f[i] involves closures, it will be evaluated at a later time, at a time when the for loop has already finished. The expression captures the variable i (and not the value of the variable i). When it is eventually evaluated, it finds the value Model.photos.f.Count in the variable i. So all hidden fields and textboxes will use the same invalid value of i.
Your second code sample avoids this problem by using a local variable within the for loop.
Related
I have the following IEnumerable
public class AuditDetailsViewModel
{
public string CustAddress { get; set; }
public IEnumerable<DefectDetailsViewModel> DefectDetails { get; set; }
}
public class DefectDetailsViewModel
{
public string Description { get; set; }
public string Comments { get; set; }
}
In my razor view how I can enumerate over this using the Html helpers? If it was a list I could do the something like the following
#model AIS.Web.Areas.Inspector.ViewModel.AuditDetailsViewModel
#for (int i = 0; i < Model.DefectDetails.Count(); i++)
{
#Html.TextBoxFor(m => m.DefectDetails[i].Description)
}
but how can I do that if the viewmodel is an IEnumerable?
I use a foreach when looping through an ienumerable
foreach(var temp in Model.DefectDetails){
#Html.TextBoxFor(x => temp.Description)
}
Hopefully this helps
You will have to use ToList()
var items = Model.DefectDetails.ToList();
#for (int i = 0; i < items.Count; i++)
{
#Html.TextBoxFor(m => m.Description[i].ToBeAudited)
}
EDIT:
You cannot apply indexing to an IEnumerable, that is true, which is why you must transform it into a List by using ToList().
If you use a for..each loop, then when you postback to the controller, the content in the textboxes will not be transferred. You must use a for...loop with an indexer, else the MVC model binder cannot bind the data correctly.
In your code, you're not calling ToList() before iterating over the collection, hence your getting the error that it cannot apply indexing to an enumerable.
I'm trying to query for a particular column & to show the item list in view properly one after another. Here is my code:
Controller:
public ActionResult ShowImage()
{
using (var context = new ImageTrialDBEntities())
{
var pathlist = (from s in context.Images
select s.ImageLink).ToList();
var model = new ImageModel();
model.ImageList = pathlist;
return View(model);
}
}
Model:
public class ImageModel
{
public string Image { get; set; }
public IList<string> ImageList { get; set; }
}
View:
<div>
#foreach (var s in Model.ImageList)
{
#Html.DisplayFor(x=>x.ImageList)
<br />
}
</div>
The list is showing like this:
I would like to show one at a time with a break in between. Please help.
Replace
#Html.DisplayFor(x=>x.ImageList)
with
#Html.DisplayFor(x=>s)
You have 2 loops in the view code. Try just printing out the variable s.
I had two fields some thing like phone number and mobile number. Some thing like..
[Required]
public string Phone { get; set; }
[Required]
public string Mobile{ get; set; }
But user can enter data in either one of it. One is mandatory. How to handle them i.e how to disable the required field validator for one field when user enter data in another field and viceversa. In which event i have to handle it in javascript and what are the scripts i need to add for this. Can anyone please help to find the solution...
One possibility is to write a custom validation attribute:
public class RequiredIfOtherFieldIsNullAttribute : ValidationAttribute, IClientValidatable
{
private readonly string _otherProperty;
public RequiredIfOtherFieldIsNullAttribute(string otherProperty)
{
_otherProperty = otherProperty;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var property = validationContext.ObjectType.GetProperty(_otherProperty);
if (property == null)
{
return new ValidationResult(string.Format(
CultureInfo.CurrentCulture,
"Unknown property {0}",
new[] { _otherProperty }
));
}
var otherPropertyValue = property.GetValue(validationContext.ObjectInstance, null);
if (otherPropertyValue == null || otherPropertyValue as string == string.Empty)
{
if (value == null || value as string == string.Empty)
{
return new ValidationResult(string.Format(
CultureInfo.CurrentCulture,
FormatErrorMessage(validationContext.DisplayName),
new[] { _otherProperty }
));
}
}
return null;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
ValidationType = "requiredif",
};
rule.ValidationParameters.Add("other", _otherProperty);
yield return rule;
}
}
which you would apply to one of the properties of your view model:
public class MyViewModel
{
[RequiredIfOtherFieldIsNull("Mobile")]
public string Phone { get; set; }
public string Mobile { get; set; }
}
then you could have a controller:
public class HomeController : Controller
{
public ActionResult Index()
{
return View(new MyViewModel());
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
return View(model);
}
}
and finally a view in which you will register an adapter to wire the client side validation for this custom rule:
#model MyViewModel
<script src="#Url.Content("~/Scripts/jquery.validate.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.js")" type="text/javascript"></script>
<script type="text/javascript">
jQuery.validator.unobtrusive.adapters.add(
'requiredif', ['other'], function (options) {
var getModelPrefix = function (fieldName) {
return fieldName.substr(0, fieldName.lastIndexOf('.') + 1);
}
var appendModelPrefix = function (value, prefix) {
if (value.indexOf('*.') === 0) {
value = value.replace('*.', prefix);
}
return value;
}
var prefix = getModelPrefix(options.element.name),
other = options.params.other,
fullOtherName = appendModelPrefix(other, prefix),
element = $(options.form).find(':input[name="' + fullOtherName + '"]')[0];
options.rules['requiredif'] = element;
if (options.message) {
options.messages['requiredif'] = options.message;
}
}
);
jQuery.validator.addMethod('requiredif', function (value, element, params) {
var otherValue = $(params).val();
if (otherValue != null && otherValue != '') {
return true;
}
return value != null && value != '';
}, '');
</script>
#using (Html.BeginForm())
{
<div>
#Html.LabelFor(x => x.Phone)
#Html.EditorFor(x => x.Phone)
#Html.ValidationMessageFor(x => x.Phone)
</div>
<div>
#Html.LabelFor(x => x.Mobile)
#Html.EditorFor(x => x.Mobile)
#Html.ValidationMessageFor(x => x.Mobile)
</div>
<button type="submit">OK</button>
}
Pretty sick stuff for something so extremely easy as validation rule that we encounter in our everyday lives. I don't know what the designers of ASP.NET MVC have been thinking when they decided to pick a declarative approach for validation instead of imperative.
Anyway, that's why I use FluentValidation.NET instead of data annotations to perform validations on my models. Implementing such simple validation scenarios is implemented in a way that it should be - simple.
I know this question is not so hot, because it was asked relatively long time ago, nevertheless I'm going to share with a slightly different idea of solving such an issue. I decided to implement mechanism which provides conditional attributes to calculate validation results based on other properties values and relations between them, which are defined in logical expressions.
Your problem can be defined and automatically solved by the usage of following annotations:
[RequiredIf("Mobile == null",
ErrorMessage = "At least email or phone should be provided.")]
public string Phone{ get; set; }
[RequiredIf("Phone == null",
ErrorMessage = "At least email or phone should be provided.")]
public string Mobile { get; set; }
If you feel it would be useful for your purposes, more information about ExpressiveAnnotations library can be found here. Client side validation is also supported out of the box.
Since nobody else suggested it, I'm going to tell you a different way to do this that we use.
If you create a notmapped field of a custom data type (in my example, a pair of gps points), you can put the validator on that and you don't even need to use reflection to get all the values.
[NotMapped]
[DCGps]
public GPS EntryPoint
{
get
{
return new GPS(EntryPointLat, EntryPointLon);
}
}
and the class, a standard getter/setter
public class GPS
{
public decimal? lat { get; set; }
public decimal? lon { get; set; }
public GPS(decimal? lat, decimal? lon)
{
this.lat = lat;
this.lon = lon;
}
}
and now the validator:
public class DCGps : DCValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (!(value is GPS)) {
return new ValidationResult("DCGps: This annotation only works with fields with the data type GPS.");
}
//value stored in the field.
//these come through as zero or emptry string. Normalize to ""
string lonValue = ((GPS)value).lonstring == "0" ? "" : ((GPS)value).lonstring;
string latValue = ((GPS)value).latstring == "0" ? "" : ((GPS)value).latstring;
//place validation code here. You have access to both values.
//If you have a ton of values to validate, you can do them all at once this way.
}
}
I have a ViewModel wrapping two complex types:
public class EditProductViewModel
{
public ProductData ProductData { get; set; }
public FridgeContent FridgeContent { get; set; }
}
and this view:
#model EditProductViewModel
#using (Html.BeginForm("Edit", "ProductData", FormMethod.Post))
{
#Html.EditorForModel()
[...]
}
ProductData and FridgeContent contain POCO properties with DataAnnotations like this:
public class FridgeContentMetadata : DatabaseEntityMetadataBase
{
[Required]
[HiddenInput(DisplayValue = false)]
public int ProductDataId { get; set; }
[Required]
[UIHint("StringReadOnly")]
public int ScaleId { get; set; }
[Required]
[UIHint("StringReadOnly")]
[Range(0.01, float.MaxValue, ErrorMessage = "The weight of a product must be positive.")]
public float Weight { get; set; }
[...]
}
I want to edit both ProductData and FridgeContent in the EditProductView using the appropriate data annotations from those classes and the EditorForModel() method (I don't want to generate the templates myself). I therefore created the templates ProductData.cshtml and FridgeContent.cshtml in /Views/Shared/EditorTemplates/:
#model FridgeContent
#Html.EditorForModel()
Unfortunately, the view for EditProductViewModel is empty (no errors raised). If I use EditorForModel for either FridgeContent or ProductData alone, it's working fine. I also tried adding [UIHInt("..")] annotations to EditProductViewModel but that doesn't make a difference.
What am I missing?
#model EditProductViewModel
#using (Html.BeginForm("Edit", "ProductData", FormMethod.Post))
{
#Html.EditorFor(o=> o.ProductData )
#Html.EditorFor(o=> o.FridgeContent )
}
or create an edit template for you ViewModel containing these two lines
#Html.EditorFor(o=> o.ProductData )
#Html.EditorFor(o=> o.FridgeContent )
UPADTE:
Oh got it finally because the rendering engine will not go more that one step in object hierarchy, you can find it in asp.net mvc code also.
Check the MVC 3.0 Source Code Here:
There is a file named DefaultEditorTemplates.cs which contains this method:
internal static string ObjectTemplate(HtmlHelper html, TemplateHelpers.TemplateHelperDelegate templateHelper) {
ViewDataDictionary viewData = html.ViewContext.ViewData;
TemplateInfo templateInfo = viewData.TemplateInfo;
ModelMetadata modelMetadata = viewData.ModelMetadata;
StringBuilder builder = new StringBuilder();
if (templateInfo.TemplateDepth > 1) { // DDB #224751
return modelMetadata.Model == null ? modelMetadata.NullDisplayText : modelMetadata.SimpleDisplayText;
}
foreach (ModelMetadata propertyMetadata in modelMetadata.Properties.Where(pm => ShouldShow(pm, templateInfo))) {
if (!propertyMetadata.HideSurroundingHtml) {
string label = LabelExtensions.LabelHelper(html, propertyMetadata, propertyMetadata.PropertyName).ToHtmlString();
if (!String.IsNullOrEmpty(label)) {
builder.AppendFormat(CultureInfo.InvariantCulture, "<div class=\"editor-label\">{0}</div>\r\n", label);
}
builder.Append("<div class=\"editor-field\">");
}
builder.Append(templateHelper(html, propertyMetadata, propertyMetadata.PropertyName, null /* templateName */, DataBoundControlMode.Edit, null /* additionalViewData */));
if (!propertyMetadata.HideSurroundingHtml) {
builder.Append(" ");
builder.Append(html.ValidationMessage(propertyMetadata.PropertyName));
builder.Append("</div>\r\n");
}
}
return builder.ToString();
}
which clearly states that if the TemplateDepth > 1 just render a simple text.
As the above answer shows, this problem seems related to the framework limiting the depth of nesting it will consider.
One way to work around the problem is to use your own editor template. Create the partial view, Object.cshtml, in Views/Shared/EditorTemplates. Here's an example template taken from here:
#{
Func<ModelMetadata, bool> ShouldShow = metadata =>
metadata.ShowForEdit && !ViewData.TemplateInfo.Visited(metadata);
}
#if (ViewData.TemplateInfo.TemplateDepth > 5) {
if (Model == null) {
#ViewData.ModelMetadata.NullDisplayText
} else {
#ViewData.ModelMetadata.SimpleDisplayText
}
} else {
foreach (var prop in ViewData.ModelMetadata.Properties.Where(ShouldShow)) {
if (prop.HideSurroundingHtml) {
#Html.Editor(prop.PropertyName)
} else {
if (string.IsNullOrEmpty(Html.Label(prop.PropertyName).ToHtmlString())==false) {
<div class="editor-label">
#Html.Label(prop.PropertyName)
</div>
}
<div class="editor-field">
#Html.Editor(prop.PropertyName)
#Html.ValidationMessage(prop.PropertyName)
</div>
}
}
}
In the above example, you can set the maximum nesting depth by changing the 5 constant.
I'm getting back to an MVC3 project after a 3 month hiatus. I need to display a drop down list that pulls from Database A, but saves to Database B. The property I need to persist is the NAICS/SIC code. Right now I just provide the user a text box to key in freeform text. So, I have the mechanics of that down. But instead it should provide only a valid list of codes from a source database.
The tricky thing to is I'm using a custom model binder to generate my ViewModels on the fly, so I don't have a distinct .cshtml file to customize.
[Serializable]
public class Step4ViewModel : IStepViewModel
{
public Step4ViewModel()
{
}
//load naics codes from somewhere
[Display(Name = "Describe the nature of your business.")]
public String NatureOfBusiness { get; set; }
[Display(Name="NAICS/SIC CODE")]
public String BusinessTypeCode { get; set; }
Tricky ViewModel
#using Microsoft.Web.Mvc;
#using Tangible.Models;
#model Tangible.Models.WizardViewModel
#{
var currentStep = Model.Steps[Model.CurrentStepIndex];
var progress = ((Double)(Model.CurrentStepIndex) / Model.Steps.Count) * 100;
}
<script type="text/javascript">
$(function () {
$("#progressbar").progressbar({
value: #progress
});
});
</script>
<div id="progressbar" style="height:20px;">
<span style="position:absolute;line-height:1.2em; margin-left:10px;">Step #(Model.CurrentStepIndex + 1) out of #Model.Steps.Count</span>
</div>
#Html.ValidationSummary()
#using (Html.BeginForm())
{
#Html.Serialize("wizard", Model)
#Html.Hidden("StepType", Model.Steps[Model.CurrentStepIndex].GetType())
#Html.EditorFor(x => currentStep, null, "")
if (Model.CurrentStepIndex > 0)
{
<input type="submit" value="Previous" name="prev" />
}
if (Model.CurrentStepIndex < Model.Steps.Count - 1)
{
<input type="submit" value="Save & Continue" name="next" />
}
else
{
<input type="submit" value="Finish" name="finish" />
}
#*<input type="submit" value="Save" name="Save" />*#
}
Controller
[HttpPost]
public ActionResult Index([Deserialize] WizardViewModel wizard, IStepViewModel step)
{
wizard.Steps[wizard.CurrentStepIndex] = step;
if (ModelState.IsValid)
{
//Always save.
var obj = new dr405();
//wire up to domain model;
foreach (var s in wizard.Steps)
{
Mapper.Map(s,obj,s.GetType(), typeof(dr405));
}
using (var service = new DR405Service())
{
//Do something with a service here.
service.Save(db, obj);
}
if (!string.IsNullOrEmpty(Request["next"]))
{
wizard.CurrentStepIndex++;
}
else if (!string.IsNullOrEmpty(Request["prev"]))
{
wizard.CurrentStepIndex--;
}
else
{
return View("Upload", obj);
}
}
else if (!string.IsNullOrEmpty(Request["prev"]))
{
wizard.CurrentStepIndex--;
}
return View(wizard);
}
WizardViewModel
[Serializable]
public class WizardViewModel
{
public String AccountNumber { get; set; }
public int CurrentStepIndex { get; set; }
public Boolean IsInitialized { get { return _isInitialized; } }
public IList<IStepViewModel> Steps { get; set; }
private Boolean _isInitialized = false;
public void Initialize()
{
try
{
Steps = typeof(IStepViewModel)
.Assembly.GetTypes().Where(t => !t.IsAbstract && typeof(IStepViewModel).IsAssignableFrom(t)).Select(t => (IStepViewModel)Activator.CreateInstance(t)).ToList();
_isInitialized = true;
//rewrite this. get the profile and wire them up or something.
this.AccountNumber = Tangible.Profiles.DR405Profile.CurrentUser.TangiblePropertyId;
}
catch (Exception e)
{
_isInitialized = false;
}
}
}
You can specify a template for a specific property on your view model by adding the UIHint attribute to the field. Since your view calls EditorFor on the model it will use the template you specified with UIHint.
BusinessTypeDropdown.ascx - (placed in Views/Shared/EditorTemplates
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<string>" %>
<% var businessTypes = ViewData["businessTypes"] as IEnumerable<string>; %>
<%= Html.DropDownListFor(m => m , new SelectList(businessTypes, Model))%>
In your View Model
[Serializable]
public class Step4ViewModel : IStepViewModel
{
public Step4ViewModel()
{
}
//load naics codes from somewhere
[Display(Name = "Describe the nature of your business.")]
public String NatureOfBusiness { get; set; }
[Display(Name="NAICS/SIC CODE")][UIHint("BusinessTypeDropdown")]
public String BusinessTypeCode { get; set; }
Then in your controller just set ViewData["businessTypes"] to your list of business types.
Without understanding your "tricky" view model code, it will be hard to make helpful suggestions.
However, there shouldn't be much problem here. You need to somehow create your dropdown list in yoru view, and populate it from data passed from your controller.
All the work happens in your controller. Populate your list or IEnumerable or whatever data source from your first database, then in your post handler save the selection it to your second database (the second part should not be much different from what you already have).