Remove/Add asterisk for requiredif when dependent property changes value - asp.net-mvc

I have the following scenario:
Multiple requiredif DataAnnotation attributes. I made a custom label helper that renders a "*" aside for the properties that are decorated if requiredif attribute.
Now on the clientside I want to be able to hide/show if the dependent property changes value.
For a more precise example I have a model
public class Document {
public bool IsFake {get; set; }
[RequiredIf("IsFake",false,ValueComparison.IsEqual)]
public string Number{ get; set; }
}
Based on the label helper that I made I have the corresponding label for Number with a red * in the UI. When I change on the client side from the is fake to the is not fake radio button I want to hide the *.
I want to do be able to make this changes automatic and not make a script that for the known fields does that, as I have multiple cases like this.
I was thinking maybe I could write a javascript code that attaches dynamically a change event to the dependent property input and a handler that would show/hide the required mark.

This is pretty much the solution which I came up with.
It is the best I could do, but still needs some customization meaning that the span that contains the "*" it's custom for my pages' DOM.
<div class="editor-label">
<label ui-documentLabel" for="Number">Number<span class="span-required">*</span></label>
</div>
<div class="editor-field">
<input class="k-textbox ui-textbox" data-val="true" data-val-requiredif="The field Number is required!" data-val-requiredif-dependentproperty="IsFake" data-val-requiredif-dependentvalue="False" data-val-requiredif-operator="IsEqual" id="Number" name="Number" value="" type="text">
<span class="field-validation-valid" data-valmsg-for="Number" data-valmsg-replace="true"></span>
</div>
and the javascript
var clietsidevalidation = function () { };
clietsidevalidation.is = function (value1, operator, value2) {
//function that verifies that the comparison between value1 and value2 is true or not
};
clietsidevalidation.handleRequirefIf = function (container) {
$('input', container).filter(function () {
var attr = $(this).attr('data-val-requiredif');
return (typeof attr !== 'undefined' && attr !== false);
}).each(function (index, item) {
var params = new Array();
params["operator"] = $(this).attr('data-val-requiredif-operator');
params["dependentvalue"] = $(this).attr('data-val-requiredif-dependentvalue');
params["dependentproperty"] = $(this).attr('data-val-requiredif-dependentproperty');
var dependentProperty = clietsidevalidation.getName(this, params["dependentproperty"]);
var dependentTestValue = params["dependentvalue"];
var operator = params["operator"];
var dependentPropertyElement = document.getElementsByName(dependentProperty);
params["element"] = this;
$(dependentPropertyElement).on('change', { params: params }, function (e) {
var input = e.data.params.element;
var inputName = $(input).attr("name");
var $span = $('label[for=' + inputName + '] span', '.editor-label');
var dependentProperty = this;
var dependentTestValue = e.data.params["dependentvalue"];
var operator = e.data.params["operator"];
var dependentPropertyElement = $(this);
var dependentValue = null;
if (dependentPropertyElement.length > 1) {
for (var index = 0; index != dependentPropertyElement.length; index++)
if (dependentPropertyElement[index]["checked"]) {
dependentValue = dependentPropertyElement[index].value;
break;
}
if (dependentValue == null)
dependentValue = false
}
else
dependentValue = dependentPropertyElement[0].value;
if (clietsidevalidation.is(dependentValue, operator, dependentTestValue) == false) {
$span.addClass('hidden');
var $form = $span.closest("form");
// get validator object
var $validator = $form.validate();
var $errors = $form.find("span.field-validation-error[data-valmsg-for='" + inputName + "']");
// trick unobtrusive to think the elements were succesfully validated
// this removes the validation messages
//custom form our solution as the validation messages are differently configured DOM
$errors.each(function () { $validator.settings.success($('label',this)); })
// clear errors from validation
$validator.resetForm();
}
else {
$span.removeClass('hidden')
}
});
});
};
This is inspired by another post which I can't find right now, but when I do I will post a link.

Related

MVC Form with Sitecore File Post through Form returns Null

For the possible duplicate, I already know how to preview my image before uploading it, my issue as detailed below is that when I submit the Form, the image is being received as null.
I am trying to submit a form to an MVC controller that should submit a model, a string, and an Image File,
I made sure that the input has the same name as the parameter within the controller
Following is the Form Initialization code
#using (Html.BeginRouteForm(Sitecore.Mvc.Configuration.MvcSettings.SitecoreRouteName,
new
{
calendar = System.Convert.ToString(Request.QueryString["calendar"]),
ID = System.Convert.ToString(Request.QueryString["id"])
},
FormMethod.Post, new { enctype = "multipart/form-data" }))
{
//Model Input
<input style="opacity: 0;" name="EventImage" type="file" accept="image/jpeg, image/jpg" id="ImageUpload" onchange="readURL(this);" />
}
And the Controller Header
[HttpPost]
[ValidateInput(false)]
public ActionResult AddEvent(Event model, string calendar, HttpPostedFileBase EventImage)
The EventImage Parameter is being returned null and I can't seem to figure out why.
I thought that the ID might be causing the problem so I changed the name attribute to ImageUpload as well as the Parameter within the controller but to no avail as the value is still null.
Additional Info: when a User uploads an image, I let them preview it in an Image box, could that be causing it?
Thanks,
Update:
Here is the code for the readURL function
function readURL(input) {
if (input.files && input.files[0]) {
var ImageCorrect = false;
var file = input.files[0];
var reader = new FileReader();
reader.onload = function (e) {
// Concatenate our HTML image info
var ext = file.name.match(/\.([^\.]+)$/)[1];
switch (ext) {
case 'jpg':
case 'jpeg':
case 'JPG':
case 'JPEG':
{
if ((Math.round(file.size / 1024)) > 500) {
alert('Image is too Large');
}
else {
var image = new Image();
image.src = e.target.result;
image.onload = function () {
var width = parseInt(image.width);
if (width <= 500) {
$('#previewImage').attr('src', e.target.result);
$('#previewImage').show();
}
else {
alert('Image width exceeds maximum width');
}
}
}
}
break;
default:
alert('Image type not allowed')
}
}
reader.readAsDataURL(input.files[0]);
}
}
If I understood your question right you are trying to submit a file from your form to your controller and you get null in the controller.
I did this before, check the following:
cshtml (you can add your attributes to event image like JS call...etc ):
<div class="form-group">
#Sitecore.Globalization.Translate.Text("EventImage")<br>
#Html.TextBoxFor(m => m.EventImage, new { type = "file" })
</div>
Model:
[Display(Name = "Event Image")]
public HttpPostedFileBase EventImage { get; set; }
Controller Signature:
[HttpPost]
[ValidateInput(false)]
public ActionResult AddEvent(Event model)
Catching the Image field:
if (model.EventImage != null && model.EventImage.ContentLength > 0)
{
var fileName = Path.GetFileName(model.EventImage.FileName);
var tempPath = Server.MapPath("~/Temp/uploads");
var path = Path.Combine(tempPath, fileName);
if (!Directory.Exists(tempPath))
Directory.CreateDirectory(tempPath);
model.EventImage.SaveAs(path);
Sitecore.Resources.Media.MediaCreatorOptions options = new Sitecore.Resources.Media.MediaCreatorOptions();
options.FileBased = false;
options.IncludeExtensionInItemName = false;
options.KeepExisting = false;
options.Versioned = false;
options.Destination = "/sitecore/media library/Images/" + ItemUtil.ProposeValidItemName(Path.GetFileName(path));
options.Database = Sitecore.Configuration.Factory.GetDatabase(MasterDatabase);
// Now create the file
Sitecore.Resources.Media.MediaCreator creator = new Sitecore.Resources.Media.MediaCreator();
MediaItem mediaItem = creator.CreateFromFile(path, options);
ImageField _eventImage = (ImageField)_event.Fields[EventImage];
_eventImage.MediaID = mediaItem.ID;
PublishItem(mediaItem);
}

MVC Custom Validation for List on Client Side

I'm trying to write a custom validator that works on the client side that validates that all checkboxes have been ticked.
Here's the declaration on the model:
[DeclarationsAccepted(ErrorMessage = "You must tick all declarations")]
public IList<DeclarationQuestion> DeclarationQuestions { get; set; }
And here's the attribute:
public class DeclarationsAccepted : ValidationAttribute, IClientValidatable
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var questions = value as IList<DeclarationQuestion>;
if (questions != null && questions.All(c => c.Answer))
{
return ValidationResult.Success;
}
return new ValidationResult("You must accepted all declarations to continue");
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var modelClientValidationRule = new ModelClientValidationRule
{
ValidationType = "declarationsaccepted",
ErrorMessage = FormatErrorMessage(metadata.DisplayName)
};
yield return modelClientValidationRule;
}
}
So far so good, works server side.
For the client I'm wiring this up as follows:
jQuery.validator.addMethod('declarationsaccepted', function (value, element, params) {
//Implement logic here to check all boxes are ticked
console.log(value);
return false;
}, '');
jQuery.validator.unobtrusive.adapters.add('declarationsaccepted', {}, function (options) {
options.rules['declarationsaccepted'] = true;
options.messages['declarationsaccepted'] = options.message;
});
I'm displaying the checkboxes like this:
#{ var questionIndex = 0; }
#foreach (var question in Model.DeclarationQuestions)
{
#Html.CheckBoxFor(model => Model.DeclarationQuestions[questionIndex].Answer, new { id = "DeclarationQuestions" + questionIndex})
questionIndex++;
}
And then displaying the validation message using this:
#Html.ValidationMessageFor(c => c.DeclarationQuestions)
When I submit the form the message is displayed but only after a post back to the server. Is there any way to get this to work on the client side?
The reason you will not get client side validation is because the html helpers generate data-val-* attributes for controls associated with properties. jquery.validate.unobtrusive reads those attributes when the form is parsed and using rules, displays an error message in the appropriate element generated by ValidationMessageFor() associated with that control (it does this by matching up the id attributes of the elements - the error message is generated in a span with <span for="TheIdOfTheAssociatedControl" ...>).
You don't (and cant) generate a control for property DeclarationQuestions (only for properties of each item in DeclarationQuestions so there is nothing that can be matched up.
You could handle this by including your own error message placeholder and intercepting the .submit event
html (add css to style #conditions-error as display:none;)
<span id="delarations-error" class="field-validation-error">
<span>You must accept all declarations to continue.</span>
</span>
Script
var declarationsError = $('#delarations-error');
$('form').submit(function() {
var isValid = $('.yourCheckBoxClass').not(':checked') == 0;
if(!isValid) {
declarationsError.show(); // display error message
return false; // prevent submit
}
});
$('.yourCheckBoxClass').change(function() {
if($(this).is(':checked')) {
declarationsError.hide(); // hide error message
}
});
Side note: You loop for generating the checkboxes should be
for (int i = 0; i < Model.DeclarationQuestions.Count; i++)
{
#Html.CheckBoxFor(m => m.DeclarationQuestions[i].Answer, new { id = "DeclarationQuestions" + i})
}

mvc controlling two dropdownlist to filter data

hello everyone I want to ask a question abuout mvc dropdownlist. I am trying to filter the datas to their places or their codes while dropdowns selected index changed. I can do it when I use one dropdown but when I use more than one dropdown I cannot get the results separately.
As you see in the picture I have two dropdownlist.
public ActionResult Index(int? id,int? ddwList)
{
Repository<Order> _ro = new Repository<Order>();
IEnumerable<SelectListItem> _orderSelectListItem = _ro.All().AsEnumerable().Select(s => new SelectListItem
{
Text = s.code,
Value = s.id.ToString()
});
ViewData["ddOrder"] = _orderSelectListItem;
Repository<Workshop> _rw = new Repository<Workshop>();
IEnumerable<SelectListItem> _workshopSelectListItem = _rw.All().AsEnumerable().Select(s => new SelectListItem
{
Text = s.name,
Value = s.id.ToString()
});
ViewData["ddwList"] = _workshopSelectListItem;
Repository<ClothShipment> _rcs = new Repository<ClothShipment>();
IEnumerable<MyClothShipment> _myClothShipment = null;
if (id != null)
{
int? idOrd = _rcs.Find(w => w.orderId == id).orderId;
//int? idWork = _rcs.Find(w => w.workshopId == id).workshopId;
if (idOrd != null)
{
_myClothShipment = _rcs.All().Where(w => w.orderId == id).Select(s => new MyClothShipment
{
id = s.id,
amount = s.amount,
orderName = s.order.code,
clothName = s.clothList.name,
workshopName = s.workshop.name,
shipDate = s.shipDate
});
}
//else if(idWork != null){
// _myClothShipment = _rcs.All().Where(w => w.workshopId == id).Select(s => new MyClothShipment
// {
// id = s.id,
// amount = s.amount,
// orderName = s.order.code,
// clothName = s.clothList.name,
// workshopName = s.workshop.name,
// shipDate = s.shipDate
// });
//}
}
else {
_myClothShipment = _rcs.All().Select(s => new MyClothShipment
{
id = s.id,
amount = s.amount,
orderName = s.order.code,
clothName = s.clothList.name,
workshopName = s.workshop.name,
shipDate = s.shipDate
});
}
return View(_myClothShipment);
}
my view is here
<div id="sample_editable_2_length" class="dataTables_length">
<label>
#Html.DropDownList("ddwList",(IEnumerable<SelectListItem>)ViewData["ddwList"],"Atölye Seçiniz",new {#id="StateDropDown1", #class = "span15 chosen"})
</label>
</div>
my view is here
<div id="sample_editable_2_length" class="dataTables_length">
<label>
#Html.DropDownList("ddwList",(IEnumerable<SelectListItem>)ViewData["ddwList"],"Atölye Seçiniz",new {#id="StateDropDown1", #class = "span15 chosen"})
</label>
</div>
<div id="sample_editable_1_length" class="dataTables_length">
<label>
#*<select class="m-wrap small" name="sample_editable_1_length" size="1" aria-controls="sample_editable_1">
</select>*#
#Html.DropDownList("ddOrder",(IEnumerable<SelectListItem>)ViewData["ddOrder"],"Sipariş Kodu Seçiniz",new {#id="StateDropDown", #class = "span15 chosen"})
</label>
</div>
and here is my script code
$("#StateDropDown").change(function (e) {
var controllerName = '#ViewContext.RouteData.Values["Controller"].ToString()';
var actionName = '#ViewContext.RouteData.Values["Action"].ToString()';
var _id = $("#StateDropDown").val();
var _url = "/" + controllerName + "/" + actionName + "/" + _id;
window.location.href =_url
});
$("#StateDropDown1").change(function (e) {
var controllerName = '#ViewContext.RouteData.Values["Controller"].ToString()';
var actionName = '#ViewContext.RouteData.Values["Action"].ToString()';
var _id = $("#StateDropDown1").val();
var _url = "/" + controllerName + "/" + actionName + "/" + _id;
window.location.href = _url
});
I am filling the dropdowns when on page load from database and getting all the data to show with dropdowns I want to filter the data that shown... And with this code one of my dropdown works I am taking the id of selected item (Index(int? id)) in here but when I try to use both of them separately it doesnt work how can I make both of them work. What should I do ? The second parameter always comes null or if I use different parameter except "id" it is again coming null ? and also I tried to take parameter as string but it also came null... Thank you for your helps.
To explain what your code is doing:
When your select a value from your first select, you are passing its value to the Index method (e.g. /Index/1) so the value of parameter id is 1 but no value has been passed to parameter ddwList so it is null. When you select a value from the second select you are passing its value to the index method (e.d. /Index/5) so the value of parameter id is 5 but no value has been passed to parameter ddwList so again it is null.
Assuming you want to display a table based on the selections of both selects, then you need to construct the url as /Index?id=1&ddwList=5. Therefore, remove the change events from your selects and add a button that constructs the query in its click event. However, the way you are doing this is reloading the whole page each time. I suggest you consider loading the table from a partial view using a jQuery.get() method to avoid a complete page load each time. For example
public ActionResult Index()
{
// Build the 2 select lists only
return View();
}
Index view
// Add the two #Html.DropdownFor()...
<button id="LoadTable">Load Table</button>
<div id="TablePlaceholder"></div>
and the script
$('#LoadTable').click(function() {
var id1 = // the value of the first select
var id2 = // the value of your second select
$.get('#Url.Action("Table")', { id: id1, ddwList: id2 }, function(data) {
$('#TablePlaceHolder').html(data);
});
}
and the partial result
public ActionResult Table(int? id, int? ddwList)
{
// Build your table view based on values of id and ddwList
return PartialView();
}
If I understand this, you are able to use one of the drop downs at a time and that drop down successfully sends it's value to your controller while the other drop down always sends null. If that's the case, that is working correctly with the way you made your dropdownlists with an option label.
As explained in the msdn documentation:http://msdn.microsoft.com/en-us/library/dd492256(v=vs.118).aspx
optionLabel Type: System.String The text for a default empty item.
This parameter can be null.
So if you want both of the drop down lists to be usable at the same time, you'll need to remove the events for .change and add a form with those dropdowns AND a submit button within to use both values at the same time.
OR
Do not use an option label meaning the first option of the dropDownList will be used as it's initial/default value. Here is a link to the msdn docs showing the different ways to format the Html.DropDownList helper: http://msdn.microsoft.com/en-us/library/system.web.mvc.html.selectextensions.dropdownlist(v=vs.118).aspx
I hope I understood you correctly!

ASP.NET MVC Validation form with AngularJS

I'm with a project in MVC 4 and AngularJS (+ twitter bootstrap). I usually use in my MVC projects "jQuery.Validate", "DataAnnotations" and "Razor". Then I enable these keys in my web.config to validate properties of model on the client:
<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
For example if I have in my model this:
[Required]
[Display(Name = "Your name")]
public string Name { get; set; }
With this Cshtml:
#Html.LabelFor(model => model.Name)
#Html.TextBoxFor(model => model.Name)
#Html.ValidationMessageFor(model => model.Name)
The html result would:
<label for="Name">Your name</label>
<input data-val="true" data-val-required="The field Your name is required." id="Name" name="Name" type="text" value="" />
<span class="field-validation-valid" data-valmsg-for="Name" data-valmsg-replace="true"></span>
But now when I use AngularJS, I want to render maybe like this:
<label for="Name">Your name</label>
<input type="text" ng-model="Name" id="Name" name="Name" required />
<div ng-show="form.Name.$invalid">
<span ng-show="form.Name.$error.required">The field Your name is required</span>
</div>
I do not know if there are any helper or "Data Annotation" to resolve this. I understand that AngularJS has many more features like:
<div ng-show="form.uEmail.$dirty && form.uEmail.$invalid">Invalid:
<span ng-show="form.uEmail.$error.required">Tell us your email.</span>
<span ng-show="form.uEmail.$error.email">This is not a valid email.</span>
</div>
Well, specifically. I need some helper or "Data Annotation" to resolve the attributes (Data Annotation) for display on the client with AngularJS.
If it still does not exist, perhaps it is time to do, like RazorForAngularJS
Edit
I think perhaps the best way to work with ASP.NET MVC and AngularJS is do it (front-end) by hand (writing all the HTML by hand)
As someone that's authored an ASP.Net/Angular website, I can tell you that you're going to be way better off stepping away from using Razor to render your HTML where you can.
In my projects I've set up one razor view to render my main page (I'm using a single page app written in Angular), then I have a folder of straight .html files that I use as my templates for Angular.
The rest is done in ASP.Net Web API calls in my case, but you can also use MVC action with JSON results.
As soon as I switched to this architecture, things went a lot more smoothly for me, development wise.
I agree with blesh idea about stepping away from razor, but you can create some tools for creating pages more rapid. IMHO it is better to use razor features where they needed instead of removing it from out toolset.
BTW have a look at ngval. It brings data annotations to client side as angularjs validators. It has an html helper and an angular module. I have to mention that project is in early development stages.
I wrote a directive to smooth out the transition from MVC to AngularJs. The markup looks like:
<validated-input name="username" display="User Name" ng-model="model.username" required>
Which behaves identically to Razor conventions, including delaying validation until after a field is modified. Over time, I've found maintaining my markup pretty intuitive and simple.
My article on the subject
Plinkr
I think there are probably half a dozen ways to do what you want. Probably the easiest is to use an Angular directive that recognizes jquery.validation markup.
Here is such a project: https://github.com/mdekrey/unobtrusive-angular-validation
And here is another: https://github.com/danicomas/angular-jquery-validate
I haven't tried either because personally, I solved this problem by writing code to make MVC output angular validation attributes instead of jquery.validation.unobtrusive attributes.
A 3rd option is to rely only on server side validation. Though this is obviously slower, it may be your only option sometimes for more complex validation scenarios. In this case, you just have to write javascript to parse the ModelStateDictionary object that Web API controllers usually return. There are some examples out there on how to do that and integrate it into AngularJS's native validation model.
Here is some incomplete code to parse the ModelStateDictionary:
````
angular.module('app')
.directive('joshServerValidate', ['$http', function ($http) {
return {
require: 'ngModel',
link: function (scope, ele, attrs, c) {
console.info('wiring up ' + attrs.ngModel + ' to controller ' + c.$name);
scope.$watch('modelState', function () {
if (scope.modelState == null) return;
var modelStateKey = attrs.joshServerValidate || attrs.ngModel;
modelStateKey = modelStateKey.replace(attrs.joshServerValidatePrefix, '');
modelStateKey = modelStateKey.replace('$index', scope.$index);
modelStateKey = modelStateKey.replace('model.', '');
console.info('validation for ' + modelStateKey);
if (scope.modelState[modelStateKey]) {
c.$setValidity('server', false);
c.$error.server = scope.modelState[modelStateKey];
} else {
c.$setValidity('server', true);
}
});
}
};
}]);
````
I'm rather disappointed with the other answers provided here. "Don't do it" isn't such a great suggestion when you're trying to validate something a little more difficult than an email address.
I solved this in a slightly different way. I modified my MVC application to response to the application/json content type via a filter and a custom view engine which injects a Json serializer razor template into the view locations to search.
This was done to allow skinning of our website with jQuery UI, Bootstrap & Json responses for the same controllers/actions.
Here is a sample json result:
{
"sid": "33b336e5-733a-435d-ad11-a79fdc1e25df",
"form": {
"id": 293021,
"disableValidation": false,
"phone": null,
"zipCode": "60610",
"firstName": null,
"lastName": null,
"address": null,
"unit": null,
"state": "IL",
"email": null,
"yearsAtAddress": null,
"monthsAtAddress": null,
"howHeard": null
},
"errors": [
"The first name is required",
"The last name is required",
"Please enter a phone number",
"Please enter an email address"
],
"viewdata": {
"cities": [
{
"selected": false,
"text": "CHICAGO",
"value": "CHICAGO"
}
],
"counties": [
{
"selected": false,
"text": "COOK"
}
]
}
}
The filter is used to translate redirect results into a json object which passes the next url onto the calling program:
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
// if the request was application.json and the response is not json, return the current data session.
if (filterContext.HttpContext.Request.ContentType.StartsWith("application/json") &&
!(filterContext.Result is JsonResult || filterContext.Result is ContentResult))
{
if (!(filterContext.Controller is BaseController controller)) return;
string url = filterContext.HttpContext.Request.RawUrl ?? "";
if (filterContext.Result is RedirectResult redirectResult)
{
// It was a RedirectResult => we need to calculate the url
url = UrlHelper.GenerateContentUrl(redirectResult.Url, filterContext.HttpContext);
}
else if (filterContext.Result is RedirectToRouteResult routeResult)
{
// It was a RedirectToRouteResult => we need to calculate
// the target url
url = UrlHelper.GenerateUrl(routeResult.RouteName, null, null, routeResult.RouteValues, RouteTable.Routes,
filterContext.RequestContext, false);
}
else
{
return;
}
var absolute = url;
var currentUri = filterContext.HttpContext.Request.Url;
if (url != null && currentUri != null && url.StartsWith("/"))
{
absolute = currentUri.Scheme + "://" + currentUri.Host + url;
}
var data = new {
nextUrl = absolute,
uid = controller.UniqueSessionId(),
errors = GetFlashMessage(filterContext.HttpContext.Session)
};
var settings = new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore
};
filterContext.Result = new ContentResult
{
ContentType = "application/json",
Content = JsonConvert.SerializeObject(data,settings)
};
}
Here is the Views\Json\Serializer.cshml, with using statements excluded for brevity and security of our codebase. This does three attempts at returning a response. The first is to read the original View{controller}{action}.cshtml, parsing out the html helpers and placing those into forms and fields. The second attempt looks for and elements from our built-in blogging system (PostContent below) and failing that we just use the Model.
#model dynamic
#{
Response.ContentType = "application/json";
Layout = "";
var session = new Object(); // removed for security purposes
var messages = ViewBag.Messages as List<string>() ?? new List<string>();
var className = "";
if (!ViewData.ModelState.IsValid)
{
messages.AddRange(ViewData.ModelState.Values.SelectMany(val => val.Errors).Select(error => error.ErrorMessage));
}
dynamic result;
string serial;
try
{
Type tModel = Model == null ? typeof(Object) : Model.GetType();
dynamic form = new ExpandoObject();
dynamic fields = new ExpandoObject();
var controller = ViewContext.RouteData.Values["controller"] as string ?? "";
var action = ViewContext.RouteData.Values["action"] as string;
var viewPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Views", controller, action + ".cshtml");
if (File.Exists(viewPath))
{
string contents = File.ReadAllText(viewPath);
var extracted = false;
var patterns = new[]
{
#"#Html\.\w+For\(\w+ => \w+\.(.*?)[,\)]",
#"#Html\.(\w+)For\(\w+ => \w+\.([\w\.]+)[, ]*(\(SelectList\))*(ViewBag\.\w+)*[^\)]*",
"name=\"(.*?)\""
};
for (var i = 0; i < 3 && !extracted; i++)
{
switch (i)
{
case 0:
form = contents.ExtractFields(patterns[0], Model as object, out extracted);
fields = contents.ExtractElements(patterns[1], Model as object, out extracted, ViewData);
break;
case 1:
form = Model as mvcApp.Models.Blog == null ? null : (Model.PostContent as string).ExtractFields(patterns[2], Model as object, out extracted);
break;
default:
form = Model;
break;
}
}
}
else if (Model == null)
{
// nothing to do here - safeModel will serialize to an empty object
}
else if (Model is IEnumerable)
{
form = new List<object>();
foreach (var element in ((IEnumerable) Model).AsQueryable()
.Cast<dynamic>())
{
form.Add(CustomExtensions.SafeClone(element));
}
} else {
form = Activator.CreateInstance(tModel);
CustomExtensions.CloneMatching(form, Model);
}
// remove any data models from the viewbag to prevent
// recursive serialization
foreach (var key in ViewData.Keys.ToArray())
{
var value = ViewData[key];
if (value is IEnumerable)
{
var enumerator = (value as IEnumerable).GetEnumerator();
value = enumerator.MoveNext() ? enumerator.Current : null;
}
if (value != null)
{
var vtype = value.GetType();
if (vtype.Namespace != null && (vtype.Namespace == "System.Data.Entity.DynamicProxies" || vtype.Namespace.EndsWith("Models")))
{
ViewData[key] = null;
}
}
}
result = new
{
uid = session.UniqueId,
form,
fields,
errors = messages.Count == 0 ? null : messages,
viewdata = ViewBag
};
var setting = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.None,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Formatting = Formatting.Indented
};
if (form is IEnumerable)
{
setting.NullValueHandling = NullValueHandling.Ignore;
}
serial = JsonConvert.SerializeObject(result, setting);
}
catch (Exception e)
{
result = new {
uid = session.UniqueId,
error = e.Message.Split('|')
};
serial = JsonConvert.SerializeObject(result);
}
#Html.Raw(serial)
}
For the clone methods see Best way to clone properties of disparate objects
public static dynamic ExtractFields(this string html, string pattern, object model, out bool extracted)
{
if (html == null || model == null)
{
extracted = false;
return null;
}
dynamic safeModel = new ExpandoObject();
var safeDict = (IDictionary<string, Object>)safeModel;
var matches = new Regex(pattern).Matches(html);
extracted = matches.Count > 0;
if ( extracted )
{
foreach (Match match in matches)
{
var name = match.Groups[1].Value;
var value = CustomExtensions.ValueForKey(model, name);
var segments = name.Split('.');
var obj = safeDict;
for (var i = 0; i < segments.Length; i++)
{
name = segments[i];
if (i == segments.Length - 1)
{
if (obj.ContainsKey(name))
{
obj[name] = value;
}
else
{
obj.Add(name, value);
}
continue;
}
if (!obj.ContainsKey(name))
{
obj.Add(name, new ExpandoObject());
}
obj = (IDictionary<string, Object>)obj[name];
}
}
}
return safeModel;
}
And here is an implementation of key value coding to make dealing with property chains a bit easier:
/// <summary>
/// This borrows KeyValueCoding from Objective-C and makes working with long chains of properties more convenient.
/// KeyValueCoding is null tolerant, and will stop if any element in the chain returns null instead of throwing a NullReferenceException.
/// Additionally, the following Linq methods are supported: First, Last, Sum & Average.
/// <br/>
/// KeyValueCoding flattens nested enumerable types, but will only aggregate the last element: "children.grandchildren.first" will return
/// the first grandchild for each child. If you want to return a single grandchild, use "first.children.grandchildren". The same applies to
/// Sum and Average.
/// </summary>
/// <param name="source">any object</param>
/// <param name="keyPath">the path to a descendant property or method "child.grandchild.greatgrandchild".</param>
/// <param name="throwErrors">optional - defaults to supressing errors</param>
/// <returns>returns the specified descendant. If intermediate properties are IEnumerable (Lists, Arrays, Collections), the result *should be* IEnumerable</returns>
public static object ValueForKey(this object source, string keyPath, bool throwErrors = false)
{
try
{
while (true)
{
if (source == null || keyPath == null) return null;
if (keyPath == "") return source;
var segments = keyPath.Split('.');
var type = source.GetType();
var first = segments.First();
var property = type.GetProperty(first);
object value = null;
if (property == null)
{
var method = type.GetMethod(first);
if (method != null)
{
value = method.Invoke(source, null);
}
}
else
{
value = property.GetValue(source, null);
}
if (segments.Length == 1) return value;
var children = string.Join(".", segments.Skip(1));
if (value is IEnumerable || "First|Last|Sum|Average".IndexOf(first, StringComparison.OrdinalIgnoreCase) > -1)
{
var firstChild = children.Split('.').First();
var grandchildren = string.Join(".", children.Split('.').Skip(1));
if (value == null) {
var childValue = source.ValueForKey(children);
value = childValue as IEnumerable<object>;
switch (first.Proper())
{
case "First":
return value == null ? childValue : ((IEnumerable<object>)value).FirstOrDefault();
case "Last":
return value == null ? childValue : ((IEnumerable<object>)value).LastOrDefault();
case "Count":
return value == null ? (childValue == null ? 0 : 1) : (int?)((IEnumerable<object>)value).Count();
case "Sum":
return value == null
? Convert.ToDecimal(childValue ?? "0")
: ((IEnumerable<object>) value).Sum(obj => Convert.ToDecimal(obj ?? "0"));
case "Average":
return value == null
? Convert.ToDecimal(childValue ?? "0")
: ((IEnumerable<object>) value).Average(obj => Convert.ToDecimal(obj ?? "0"));
}
} else {
switch (firstChild.Proper())
{
case "First":
return ((IEnumerable<object>)value).FirstOrDefault().ValueForKey(grandchildren);
case "Last":
return ((IEnumerable<object>)value).LastOrDefault().ValueForKey(grandchildren);
case "Count":
if (!string.IsNullOrWhiteSpace(grandchildren))
{
value = value.ValueForKey(grandchildren);
if (value != null && ! (value is IEnumerable<object>))
{
return 1;
}
}
return value == null ? 0 : ((IEnumerable<object>)value).Count();
case "Sum":
return ((IEnumerable<object>)value).Sum(obj => Convert.ToDecimal(obj.ValueForKey(grandchildren)??"0"));
case "Average":
return ((IEnumerable<object>)value).Average(obj => Convert.ToDecimal(obj.ValueForKey(grandchildren) ?? "0"));
}
}
if (value == null) return null;
var flat = new List<object>();
foreach (var element in (IEnumerable<object>)value)
{
var child = element.ValueForKey(children);
if (child == null)
{
continue;
}
if (child is IEnumerable && !(child is string))
{
flat.AddRange((IEnumerable<object>) child);
}
else
{
flat.Add(child);
}
}
return flat.Count == 0? null: flat;
}
source = value;
keyPath = children;
}
}
catch (Exception)
{
if (throwErrors) throw;
}
return null;
}

Am I doing this knockout binding incorrectly?

The view code:
<ul data-bind="foreach: BackColorOptions">
<li data-bind="css: { selected: Selected }">
<label>
<input type="radio" name="BackColorOption"
data-bind="value: Color, checked: $root.BackColor" />
</label>
</li>
</ul>
#{
var jsonModel = new System.Web.Script.Serialization.
JavaScriptSerializer().Serialize(Model);
}
<input type="hidden" id="JsonModel" value='#jsonModel' />
The viewmodel code:
var initialData = $.parseJSON($('#JsonModel').val());
function BackColorOption(data, parent) {
var self = this;
self.parent = parent;
self.Text = ko.observable(data.Text);
self.Color = ko.computed(function () {
return '#' + self.Text().toLowerCase();
});
self.Selected = ko.computed(function () {
var backColor = self.parent.BackColor();
if (backColor) {
return backColor.toLowerCase() == self.Color;
}
return false;
});
}
function TestViewModel() {
var self = this;
self.BackColor = ko.observable(initialData.BackColor);
var mappedBackColorOptions = $.map(initialData.BackColorOptions,
function (item) {
return new BackColorOption(item, self);
}
);
self.BackColorOptions = ko.observableArray(mappedBackColorOptions);
}
ko.applyBindings(new TestViewModel());
The model code:
string BackColor { get; set; }
SelectListItem[] BackColorOptions
{
get
{
return new[]
{
new SelectListItem{Text = "cc0000"},
new SelectListItem{Text = "ff9900"},
new SelectListItem{Text = "dddd33"},
new SelectListItem{Text = "009900"},
new SelectListItem{Text = "00cccc"},
new SelectListItem{Text = "0066ff"},
new SelectListItem{Text = "9900ff"},
new SelectListItem{Text = "ff00ff"},
};
}
}
The code above works as expected in IE (8) and Chrome (17), but not FF (10.0.2). I'm basically trying to do a color selector similar to GitHub's issue labels. The view renders a set of radio buttons you can click on to choose a color. When a radio is checked, I add a selected css class to the parent <li>. The css class causes a checkmark icon to appear over the <li>.
In Firefox, the selected css class is only applied after a user has gone through and checked each radio button at least once. I debugged and found that this is because of the way the self.Color computed observable is evaluated in the BackColorOption closure. Before the first time a radio is checked, typeof(self.Color) == 'function' evaluates to true. However after it is checked, typeof(self.Color) == 'string' evaluates to true.
This typeof(self.Color) behavior is the same according to both Firebug and Chrome's js debugger. However the issue in FF is because of this line in the self.Selected computed observable in the BackColorOption closure:
return backColor.toLowerCase() == self.Color;
Chrome & IE still return true even when self.Color is a function instead of a string. However Firefox does not. When self.Color is a function, it returns false. This is why you have to check each radio at least once before the css class is added to the <li> and the icon appears.
I'm still a bit new to knockout, and may not be appropriately calling a viewmodel property as a function when supposed to. I'm still a little unclear when to use the () parenthesis and when to omit them. Is there another way I should write the self.Selected computed observable, which depends on the self.Color computed observable (in the BackColorOption closure)?
Update 1
I was able to get this to work in FF 10.0.2 with the following:
self.Selected = ko.computed(function () {
var backColor = self.parent.BackColor();
var selfColor = self.Color;
if (typeof (selfColor) === 'function')
selfColor = self.Color();
if (backColor) {
return backColor.toLowerCase() === selfColor;
}
return false;
});
However, this feels like I'm fighting knockout. Isn't it supposed to "just work"?
The value binding in KO is not really ideal for radio buttons and checkboxes. In your situation you need your radio buttons to have a value attribute so that when you click them, that value can be used to update your TestViewModel.BackColor observable.
Normally with radio buttons, you don't want the value attribute to change (if ever) once the html is rendered.
So, I've changed your html template from using a value binding to instead using an attr binding (html attribute). This is now just setting the value html attribute of your radio buttons. The checked binding then keeps your TestViewModel.BackColor observable synced with whatever the value is of the radio button that is checked.
See this fiddle:
http://jsfiddle.net/m2KQ2/
Also, the line in your BackColorOption function has a typo:
self.Selected = ko.computed(function () {
var backColor = self.parent.BackColor();
if (backColor) {
return backColor.toLowerCase() == self.Color; //<-- should be Color();
}
return false;
});

Resources