WebAPI2 Model Binding not working with HTTP PUT - ajax

I'm following Scott Allen's MVC4 course on PluralSight (I'm using MVC5 and WebAPI2 but they should be the same) and I am trying to pass an object via HTTP PUT. The model binder should bind it, but I am getting NULL for the parameter.
public HttpResponseMessage PutObjective(int id, [FromBody] Objective objective)
{
if (ModelState.IsValid && id == objective.ObjectiveID)
{
//todo: update - look up id, replace text
return Request.CreateResponse(HttpStatusCode.OK, objective);
}
else
{
return Request.CreateResponse(HttpStatusCode.BadRequest);
}
}
and in my front-end javascript I am doing the following (I'm creating an object for testing, so ignore 'objective' passed in):
var updateObjective = function (objective) {
var myobj = { "ObjectiveID": "3", "ObjectiveDescription": "test" };
return $.ajax(objectiveApiUrl + "/" + objective.ObjectiveID, {
type: "PUT",
data: myobj
});
}
My class looks like this:
public class Objective
{
public int ObjectiveID { get; private set; }
public string ObjectiveDescription { get; set; }
public Objective (int Id, string Desc)
{
this.ObjectiveID = Id;
this.ObjectiveDescription = Desc;
}
}
Any thoughts on why 'objective' in the backend is always 'null' ?
I've done what Scott Allen is doing, even tried adding in [FromBody] but no luck. $.ajax should have the correct content type by default I understand, so no need to set it.
I had Fiddler2 but I'm unsure as to what I am looking at to be honest. I can see my object as JSON being sent to the backend.

Well, if you're familiar with Model Binding you'll have seen the issue in my Objective class:
public int ObjectiveID { get; private set; }
with a private set, no instance can be created of the Objective class. To make it work, the 'private' access specifier needs to be removed.
What needs to happen really is that Objective becomes ObjectiveViewModel, and we convert what comes back to an Objective domain object (which may have more properties than we need for this screen). This can have a private set.

Related

DataSourceRequest is not deserializing for a WebAPI Get method

I am trying to call a WebAPI method from Angular 5 like this:
selectClaims(state: DataSourceRequestState):Observable<DataResult>
{
return this.http.get<GridDataResult>(`${this.apiUrl}/SelectClaims?${toDataSourceRequestString(state)}`);
}
Which calls the API method as expected. The API method is:
[Route("SelectClaims")]
[HttpGet]
public IHttpActionResult SelectClaims([FromUri][DataSourceRequest]DataSourceRequest ClaimsRequest)
{
if(ClaimsRequest == null)
ClaimsRequest=new DataSourceRequest { Page=1, PageSize=20 };
var result = _db.Claims.ToDataSourceResult(ClaimsRequest, c => { c.SortHistory(); return c; });
return Ok(result);
}
The trouble is that ClaimsRequest only de-serializes Page and PageSize correctly. Filters and Sorts don't come through:
Fiddler tells me that the URL from Angular is:
GET /api/v1/Claims/SelectClaims?filter=id~eq~2&page=1&sort=firstName-asc&pageSize=20 HTTP/1.1, but in the controller both filter and sort are null.
If I create a URL through Swagger like: 'http://localhost:50223/api/v1/Claims/SelectClaims?ClaimsRequest.page=1&ClaimsRequest.pageSize=11&ClaimsRequest.sorts=firstName-desc' I do see a sort array in the API method, but the "Member" field is null.
Any attempt to add a filter through Swagger like 'http://localhost:50223/api/v1/Claims/SelectClaims?ClaimsRequest.page=1&ClaimsRequest.pageSize=11&ClaimsRequest.filters=id~eq~2' results in a "Cannot create an instance of an interface." error.
The state is a DataSourceRequestState in the angular component from a Kendo Grid for Angular.
I have simulated this in a simple test program and everything works fine there. The only difference in my test program is that the API controller targets .Net Core and the real system targets .Net 4.6.1.
Do I have to de-serialize manually in .Net 4.6.1 for some reason, or is something else going on here?
It should be a POST not a GET. Something like this:
return this.http.post<GridDataResult>(`${this.apiUrl}/SelectClaims`, toDataSourceRequestString(state)});
I needed it to be a GET (URL) so i created a new object
public class GridParamaterBinder
{
public int Page { get; set; }
public int PageSize { get; set; }
public string Filter { get; set; }
public string Sort { get; set; }
public DataSourceRequest ToDataSourceRequest(IConfigurationProvider mapper, Func<string, string> OverDefaultParamaterMapping)
{
DataSourceRequest result = new DataSourceRequest();
result.Page = Page;
result.PageSize = PageSize;
result.Sorts = GridDescriptorSerializer.Deserialize<SortDescriptor>(Sort);
result.Filters = FilterDescriptorFactory.Create(Filter);
return result;
}
}
and used it instead of the Telerik effort.
in API I Bind it like so
public virtual DataSourceResult Get([FromUri]GridParamaterBinder request)
And then used it like
DataSourceResult results = query.ToDataSourceResult(request.ToDataSourceRequest(), r => (r)));
Thanks #KevDevMan for your solution. I found this example,
then I changed my API controller like this and it worked like a charm :
[HttpGet, Route("for-kendo-grid")]
public DataSourceResult GetProducts([System.Web.Http.ModelBinding.ModelBinder(typeof(WebApiDataSourceRequestModelBinder))] DataSourceRequest request)
explanation here

WebApi - Passing an Array of Values

I need to build an API using ASP.NET Web API (version 4.5.2). To get started, I'm just trying to create a basic endpoint that adds some numbers. In an attempt to do this, I've created:
[RoutePrefix("api/test")]
public class MyController : ApiController
{
[HttpGet]
public IEnumerable<int> Calulate(decimal[] op1, decimal[] op2)
{
var results = new List<Calculation>();
for (var i=0; i<op1.Length; i++)
{
var calculation = new Calculation();
calculation.Operand1 = op1[i];
calculation.Operand2 = op2[i];
calculation.Calculate();
results.Add(calculation);
}
return results;
}
public class Calculation
{
public int Operand1 { get; set; }
public int Operand2 { get; set; }
public int Result { get; set; }
public void Calculate()
{
this.Result = this.Operand1 + this.Operand2;
}
}
}
I am now trying to hit this endpoint via the Postman Chrome app. When I run it via Postman, I'm getting an error. Here is what I'm doing:
In Postman, I've put "http://localhost:50668/api/test/calculate" in the URL field next to the "GET" drop down. I then click "Send". I'm receiving the following error:
{
"Message": "An error has occurred.",
"ExceptionMessage": "Can't bind multiple parameters ('op1' and 'op2') to the request's content.",
"ExceptionType": "System.InvalidOperationException",
"StackTrace": "..."
}
I think (I don't know) the cause is because I'm not passing the values to the API from Postman correctly. However, I'm not sure how to do that. How do I pass an array of values to an API?
Short answer
To send arrays of decimals, WebApi expects url signature like:
GET http://localhost:50668/api/test/calculate?Operand1=1.0&Operand1=2.0&Operand2=3.0&Operand2=4.0
That url will send [1.0,2.0] as Operand1 and [3.0,4.0] as Operand2.
Long answer
By calling your api using GET http://localhost:50668/api/test/calculate, you actually send nothing to your server. (aside of headers content)
If you want to send data to your server, you have (at least) 2 options:
Option 2: Use GET method if operation is idempotent
Like William Xifaras already pointed out, specify that your inputs will come from the URL so WebApi interprets properly. To do so, use [FromUri].
[HttpGet]
[Route("calculate")]
public List<Calculation> CalculateWithGet([FromUri]decimal[] Operand1, [FromUri]decimal[] Operand2)
{
var results = new List<Calculation>();
for (var i = 0; i < Operand1.Length; i++)
{
var calculation = new Calculation();
calculation.Operand1 = Operand1[i];
calculation.Operand2 = Operand2[i];
calculation.Calculate();
results.Add(calculation);
}
return results;
}
public class Calculation
{
public decimal Operand1 { get; set; }
public decimal Operand2 { get; set; }
public decimal Result { get; set; }
public void Calculate()
{
Result = this.Operand1 + this.Operand2;
}
}
With a REST client, it should look like:
With GET, data is sent via the URL
Note that if you use GET Method, the server will expect to receive inputs from the URL. You should therefore send queries like:
GET http://localhost:50668/api/test/calculate?op1=1.0&op1=2.0&op2=3.0&op2=4.0
Use POST method if operation is not idempotent
Since the operation does some server side calculation, I pretend it may not always be idempotent. If it is the case, POST might be more appropriate.
[HttpPost]
[Route("calculate")]
public List<Calculation> CalculateWithPost(CalculationInputs inputs)
{
var results = new List<Calculation>();
for (var i = 0; i < inputs.Operand2.Length; i++)
{
var calculation = new Calculation();
calculation.Operand1 = inputs.Operand1[i];
calculation.Operand2 = inputs.Operand2[i];
calculation.Calculate();
results.Add(calculation);
}
return results;
}
public class CalculationInputs
{
public decimal[] Operand1 { get; set; }
public decimal[] Operand2 { get; set; }
}
public class Calculation
{
public decimal Operand1 { get; set; }
public decimal Operand2 { get; set; }
public decimal Result { get; set; }
public void Calculate()
{
Result = this.Operand1 + this.Operand2;
}
}
With POST, data is sent via the body
With that structure, the server expects to receive inputs from the request body. WebApi will deserialize the body if it matches the signature of your function.
With a REST client, it should look like:
Sidenote
The nuget package used to get the SwaggerUI generated (printscreens) can be find here. Very useful to run adhoc tests on WebApis.
Add from [FromUri] before the parameter.
public IEnumerable<int> Calulate([FromUri] decimal[] op1, [FromUri] decimal[] op2)
To force Web API to read a complex type from the URI, add the
[FromUri] attribute to the parameter
http://www.asp.net/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api
I think you can pass as a JSON array
http://localhost:50668/api/test/calculate?op1=[1,2,3]&op2=[4,5,6]
Hope this helps

Sitecore7 Linq to Sitecore only works with SearchResultItem but not with Custom Mapped Class

I have this very weird problem that I cannot get my head around. Perhaps someone could point out what I am doing wrong.
Basically, I am just trying to search items using Linq to Sitecore.
So, my classes look like ( I am using glass too)
[SitecoreType(TemplateId = "{TEMPLATE_GIUD}")]
public class MyMappedClass : SharedFieldClass
{
[SitecoreField(FieldName = "mylist")]
public virtual IEnumerable<SharedFieldClass> MyMultilistField { get; set; }
[SitecoreField(FieldName = "field1")]
[IndexField("field1")]
public virtual MyKeyValue field1 { get; set; }
}
[SitecoreType]
public class MyKeyValue
{
public virtual Sitecore.Data.ID Id {get;set;}
public virtual string MyValue{get;set;}
}
So when I do the below query it works as it's supposed to.
var results = context.GetQueryable<SearchResultItem>()
.Where(c => ((string)c["field1"]) == "{GUID}").GetResults();
But, when I do the below it returns 0 result.
List<MyMappedClass> results = context.GetQueryable<MyMappedClass>()
.Where(c => c.field1.MyValue == "{GUID}").ToList();
I have read this link . And I have followed the 2nd process described here for Glass to work with Sitecore7 Search (the "SharedFieldClass" contains all the basic index fields).
This is a pretty obvious scenario and I'm sure lots of people have done it already and I am doing something silly here.
Thanks in advance.
/## EDIT ##/
Okay so I've done a bit more digging on this. Not sure if it's a bug in ContentSearch/Luncene.NET API or I am missing something BUT seems like what was posted here is probably not TRUE and if you have a complex field type ( which you will ) you can not query with a mapped class against Lucene. ( not sure if this is also the case for Solr). If you are doing search against simple type like string and int then it works like a charm.
SO here're my findings:
After enabling DEBUG and LOG on for contentsearch I found that if I query like this context.GetQueryable<MyMappedClass>().Where(c => c.field1.MyValue == "{GUID}") what it gets translated into is DEBUG Executing lucene query: field1.value:7e9ed2ae07194d83872f9836715eca8e and as there's no such thing in the index named "field1.value" the query doesn't return anything. The name of the index is actually just "field1". Is this a bug ??
However, query like this context.GetQueryable<SearchResultItem>() .Where(c => ((string)c["field1"]) == "{GUID}").GetResults(); works because it gets translated into "DEBUG Executing lucene query: +field1:7e9ed2ae07194d83872f9836715eca8e".
I also did write a method in my mapped class like below:
public string this[string key]
{
get
{
return key.ToLowerInvariant();
}
set { }
}
Which allowed me write the below query with my MyMappedClass.
results2 = context.GetQueryable<MyMappedClass>().Where(c => c["filed1"]== "{GUID}").ToList();
This returned expected result. BUT the values of the fields in MyMappedClass are not filled ( in fact they're all null except the core/shared values like templateid, url etc which are populated in the filled document). So the result list are pretty much useless. I could do a loop over all of them and manually get the values populated as I have the itemid. But imagine the cost for a large result set.
Lastly I did this:
<fieldType fieldTypeName="droplink" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider" />
So this returned populated "field1" with the itemid in lucene query using "IndexViewer2.0". BUT this fails for MyMappedClass too as the value of "field1" in the document is a System.string .. but it is mapped as "MyKeyValue" in MyMappedClass SO it throws the below exception
Exception: System.InvalidCastException
Message: Invalid cast from 'System.String' to 'MyLib.MyKeyValue'.
SO, the big question is:
How does one query using his/her mapped class using the cool ContentSearch API ?
I bit more further digging got me to a working solution. Posting it here just in case anyone runs into this issue.
This is how my "SharedFieldClass" looked like ( which was somewhat wrong )
public abstract class SharedFieldClass
{
[SitecoreId]
[IndexField("_id")]
[TypeConverter(typeof(IndexFieldIDValueConverter))]
public virtual ID Id { get; set; }
[SitecoreInfo(SitecoreInfoType.Language)]
[IndexField("_language")]
public virtual string Language { get; set; }
[SitecoreInfo(SitecoreInfoType.Version)]
public virtual int Version
{
get
{
return Uri == null ? 0 : Uri.Version.Number;
}
}
[TypeConverter(typeof(IndexFieldItemUriValueConverter))]
[XmlIgnore]
[IndexField("_uniqueid")]
public virtual ItemUri Uri { get; set; }
}
And there's a class in Glass that does the mapping. Which looks like below:
var sitecoreService = new SitecoreService("web");
foreach (var r in results)
{
sitecoreService.Map(r);
}
for me this class was failing to map because of this line:
[SitecoreId]
[IndexField("_id")]
[TypeConverter(typeof(IndexFieldIDValueConverter))]
public virtual ID Id { get; set; }
It was throwing a NULL exception at sitecoreService.Map(r); line
So I changed it to below:
[SitecoreId]
[IndexField("_group")]
[TypeConverter(typeof(IndexFieldIDValueConverter))]
public virtual ID Id { get; set; }
And it worked. I'm not sure when the index field for ItemId in sitecore changed from "_id" to "_group" or whether it was always like that. But it is "_group" in the SearchResultItem class. So I used it and mapping was successful.
So to Sum it all the solution looks like this:
The Mapped Class:
[SitecoreType(TemplateId = "{TEMPLATE_GIUD}")]
public class MyMappedClass : SharedFieldClass
{
[SitecoreField(FieldName = "mylist")]
public virtual IEnumerable<SharedFieldClass> MyMultilistField { get; set; }
[SitecoreField(FieldName = "field1")]
[IndexField("field1")]
public virtual MyKeyValue field1 { get; set; }
// Will be set with key and value for each field in the index document
public string this[string key]
{
get
{
return key.ToLowerInvariant();
}
set { }
}
}
[SitecoreType]
public class MyKeyValue
{
public virtual Sitecore.Data.ID Id {get;set;}
public virtual string MyValue{get;set;}
}
And the query is:
List<MyMappedClass> results = context.GetQueryable<MyMappedClass>()
.Where(c => c["field1"] == "{GUID}").ToList();
That's it.
/* edited */
OH!! Wait that's not it. This will still fail to cast the complex type "MyKeyValue" when the "field1" is populated with guid in the index document.
So to avoid this I had to write my custom converter as #Michael Edwards suggested.
I had to modify the class slightly to fit my needs ..
public class IndexFieldKeyValueModelConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
var config = Glass.Mapper.Context.Default.GetTypeConfiguration<SitecoreTypeConfiguration>(sourceType, true);
if (config != null && sourceType == typeof(MyLib.IKeyValue))
{
return true;
}
else
return base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(string))
return true;
else
return base.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
var scContext = new SitecoreContext();
Guid x = Guid.Empty;
if(value is string)
{
x = new Guid((string)value);
}
var item = scContext.Database.GetItem(x.ToString());
if (item == null)
return null;
return scContext.CreateType(typeof(MyLib.IKeyValue), item, true, false, new Dictionary<string, object>());
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
var config = Glass.Mapper.Context.Default.GetTypeConfiguration<SitecoreTypeConfiguration>(value.GetType(), true);
ID id = config.GetId(value);
return id.ToShortID().ToString().ToLowerInvariant();
}
}
Not sure if it were the expected behaviour or not .. but for some reason, in the convertfrom method the value of the "object value" parameter was short string id format. So for scContext.Database.GetItem to work I had to convert it to a proper GUID and then it started returning proper item rather than null.
AND then I wrote my query like this:
results = context.GetQueryable<MyMappedGlassClass>().Where(c => c["field1"] == field1value && c["field2"] == field2value && c["_template"] == templateId).Filter(selector => selector["_group"] != currentId).ToList();
Looks like a fair bit of work to get it to work. I guess using the LinqHelper.CreateQuery method is the easy way out .. but as Mr. Pope suggested here that this method is not be used as this is an internal method.
Not sure what's the balance here.
/* end edited */
Please see my question description section for explanation on why I had to do things this way.
Also, (I bias opinion ) is the trick described here may not be valid anymore (please see my question description's edit section for the reason behind).
Also, index field for itemid in the Glass Mapper tutorial here is I think wrong (unless otherwise proven).
Hope it helps someone saving/wasting time.
You could create a custom field mapper for lucene that would convert from the Guid in the index to a glass model. I hacked this out but I haven't tested it:
public class IndexFieldDateTimeValueConverter : TypeConverter
{
public Type RequestedType { get; set; }
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
var config = Glass.Mapper.Context.Default.GetTypeConfiguration<SitecoreTypeConfiguration>(sourceType, true);
if (config != null)
{
RequestedType = sourceType;
return true;
}
else
return base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(string))
return true;
else
return base.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
var scContext = new SitecoreContext();
return scContext.CreateType(RequestedType, scContext.Database.GetItem(value.ToString()),true, false, new Dictionary<string, object>());
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
var config = Glass.Mapper.Context.Default.GetTypeConfiguration<SitecoreTypeConfiguration>(value.GetType(), true);
ID id =config.GetId(value);
return id.ToShortID().ToString().ToLowerInvariant();
}
My concern is that the ConvertFrom method does not get passed the type requested so we have to store this as property on the class to pass it from the CanConvertFrom method to the ConvertFrom method. This makes this class not thread safe.
Add this to the indexFieldStorageValueFormatter section of the sitecore config.
The problem here is that SearchResultItem is not actually an Item, but does have the GetItem() method to get the Sitecore item. What you need to do is the following:
List<MyMappedClass> results = context.GetQueryable<SearchResultItem>()
.Select(sri => sri.GetItem())
.Where(i => i != null)
.Select(i => i.GlassCast<MyMappedClass>())
.Where(c => c.field1.MyValue == "{GUID}").ToList();
I haven't worked with Glass specifically, but if you change your parent class to SearchResultItem, does it begin working? If so, that would indicate an issue with the SharedFieldClass parent class.

Don't understand the mechanics of writing own validation attribute

I have written an attribute before, but I I have not written a validation attribute before. I am seriously confused about how it all works together. I have read most of the tutorials online about how to go about accomplishing this. But I am left with a couple of questions to ponder.
Keep in mind that I am trying to write a requiredIf attribute that will only call a remote function if a certain Jquery variable is set... which incidentally is a variable that is pulled from view state... I guess I could make that part of my view model. But I digress
1) The C# code is slightly confusing. I know my attribute should extend the ValidationAttribute, IClientValidatable class and interface respectively. But I am a little confused about what each of the overidden methods should be doing? I am trying to write a requiredIf, how does overwriting these methods help me accomplish this goal?
2) If the variable is not there, I simply don't want the remote function to attempt to validate the field. I don't want any message to pop up on my form. Alot of the tutorials seem to revolve around that.
3) I am confused about what I need to do with the jquery to add this function to the view... What do I need to add to the JQuery to get this thing to work... It seems like a lot of extra coding when I could simply just type up a jquery function that did the same thing with just the same ore less coding... I know it also adds server side validation which is good. But still...
Here is what I have for my jquery side of this equation...
(function ($) {
$validator.unobtrusive.adapters.addSingleVal("requiredifattribute", "Dependent");
$validator.addMethod("requiredifattribute", function (value, element, params) {
if (!this.optional(element)) {
var otherProp = $('#' + params)
return (otherProp.val() != value);
}
return true;
})
}(jQuery));
Here is my Attribute (which is basically carbon copied out of one the required if tutorials... I know I need to customize it more, but once I get a better idea of what every piece is doing I will do that...
[AttributeUsage(AttributeTargets.Property)]
public class RequiredIfAttribute : ValidationAttribute, IClientValidatable {
private const string errorMessage = "The {0} is required.";
//public string
private RequiredAttribute innerAttribute = new RequiredAttribute();
public string DependentProperty { get; set; }
public object TargetValue { get; set; }
public RequiredIfAttribute(string dependentProperty, object targetValue){
this.DependentProperty = dependentProperty;
this.TargetValue = targetValue;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext) {
var field = validationContext.ObjectInstance.GetType().GetProperty(DependentProperty);
if (field != null) {
var dependentValue = field.GetValue(validationContext.ObjectInstance, null);
if ((dependentValue == null && TargetValue == null) || (dependentValue.Equals(TargetValue))) {
if (!innerAttribute.IsValid(value))
return new ValidationResult(ErrorMessage);
}
}
return ValidationResult.Success;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) {
ModelClientValidationRule modelClientValidationRule = new ModelClientValidationRule {
ErrorMessage = FormatErrorMessage(metadata.DisplayName),
ValidationType = "requiredifattribute"
};
modelClientValidationRule.ValidationParameters.Add("dependent", DependentProperty);
yield return modelClientValidationRule;
}
}
UPDATE: What I have simply isn't working
Here is how a property in my model is anotated with the above attribute
[RequiredIf("isFlagSet", true)]
[Remote("ValidateHosFin", "EditEncounter", AdditionalFields = "hospitalFin, encflag", ErrorMessage = "Got Damn this is complex!")]
[MinLength(6)]
public string HostpitalFinNumber { get; set; }
The value in my view that I was trying to key this validation on is set up like so...
ViewData["ADDENCOREDITTEMP"] = encflag;
if (encflag == "AddEnc"){
isFlagSet = true;
}
I embed it into my page like so...
#Html.Hidden("isFlagSet", isFlagSet, new { id = "isFlagSet"})
I can't get my form to submit... The person who said he just tried this and got it to work, could you post the code?
Model:
public class X
{
[RequiredIf("y", "y", ErrorMessage = "y is not y")]
public string x { get; set; }
public string y { get; set; }
}
View:
#using(Html.BeginForm())
{
#Html.ValidationSummary()
#Html.TextBoxFor(m => m.x)
#Html.TextBoxFor(m => m.y)
<input type="submit"/>
}
I assume your validation fails on the server side? do you have isFlagSet property in your view model?

How to use CheckBox in View _CreateOrEdit.cshtml for an integer or character database field

MVC 3, EntityFramework 4.1, Database First, Razor customization:
I have an old database that sometimes uses Int16 or Char types for a field that must appear as a CheckBox in the MVC _CreateOrEdit.cshtml View. If it is an Int, 1=true and 0=false. If it is a Char, "Y"=true and "N"=false. This is too much for the Entity Framework to convert automatically. For the Details View, I can use:
#Html.CheckBox("SampleChkInt", Model.SampleChkInt==1?true:false)
But this won't work in place of EditorFor in the _CreateOrEdit.cshtml View.
How to do this? I was thinking of a custom HtmlHelper, but the examples I've found don't show me how to tell EntityFramework to update the database properly. There are still other such customizations that I might like to do, where the MVC View does not match the database cleanly enough for EntityFramework to do an update. Answering this question would be a good example. I am working on a sample project, using the following automatically generated (so I can't make changes to it) model class:
namespace AaWeb.Models
{
using System;
using System.Collections.Generic;
public partial class Sample
{
public int SampleId { get; set; }
public Nullable<bool> SampleChkBit { get; set; }
public Nullable<short> SampleChkInt { get; set; }
public Nullable<System.DateTime> SampleDate { get; set; }
public string SampleHtml { get; set; }
public Nullable<int> SampleInt { get; set; }
public Nullable<short> SampleYesNo { get; set; }
public string Title { get; set; }
public byte[] ConcurrencyToken { get; set; }
}
}
I figured it out. Do not need a model binder or Html Helper extension:
In _CreateOrEdit.cshtml, I made up a new name SampleChkIntBool for the checkbox, and set it according to the value of the model SampleChkInt:
#Html.CheckBox("SampleChkIntBool", Model == null ? false : ( Model.SampleChkInt == 1 ? true : false ), new { #value = "true" })
Then, in the [HttpPost] Create and Edit methods of the Sample.Controller, I use Request["SampleChkIntBool"] to get the value of SampleChkIntBool and use it to set the model SampleChkInt before saving:
string value = Request["SampleChkIntBool"];
// #Html.CheckBox always generates a hidden field of same name and value false after checkbox,
// so that something is always returned, even if the checkbox is not checked.
// Because of this, the returned string is "true,false" if checked, and I only look at the first value.
if (value.Substring(0, 4) == "true") { sample.SampleChkInt = 1; } else { sample.SampleChkInt = 0; }
I believe a custom model binder would be in order here to handle the various mappings to your model.
ASP.NET MVC Model Binder for Generic Type
etc
etc
Here is the way to go from checkbox to database, without the special code in the controller:
// The following statement added to the Application_Start method of Global.asax.cs is what makes this class apply to a specific entity:
// ModelBinders.Binders.Add(typeof(AaWeb.Models.Sample), new AaWeb.Models.SampleBinder());
// There are two ways to do this, choose one:
// 1. Declare a class that extends IModelBinder, and supply all values of the entity (a big bother).
// 2. Declare a class extending DefaultModelBinder, and check for and supply only the exceptions (much better).
// This must supply all values of the entity:
//public class SampleBinder : IModelBinder
//{
// public object BindModel(ControllerContext cc, ModelBindingContext mbc)
// {
// Sample samp = new Sample();
// samp.SampleId = System.Convert.ToInt32(cc.HttpContext.Request.Form["SampleId"]);
// // Continue to specify all of the rest of the values of the Sample entity from the form, as done in the above statement.
// // ...
// return samp;
// }
//}
// This must check the property names and supply appropriate values from the FormCollection.
// The base.BindProperty must be executed at the end, to make sure everything not specified is take care of.
public class SampleBinder : DefaultModelBinder
{
protected override void BindProperty( ControllerContext cc, ModelBindingContext mbc, System.ComponentModel.PropertyDescriptor pd)
{
if (pd.Name == "SampleChkInt")
{
// This converts the "true" or "false" of a checkbox to an integer 1 or 0 for the database.
pd.SetValue(mbc.Model, (Nullable<Int16>)(cc.HttpContext.Request.Form["SampleChkIntBool"].Substring(0, 4) == "true" ? 1 : 0));
// To do the same in the reverse direction, from database to view, use pd.GetValue(Sample object).
return;
}
// Need the following to get all of the values not specified in this BindProperty method:
base.BindProperty(cc, mbc, pd);
}
}

Resources