KendoValidation with Remote validation - asp.net-mvc

This is almost killing me. I've never wasted so much time on an issue.
I've got a field that needs to be validated with a call to a REST API since it needs to check the presence of some data on the DB. The problem I've is that calling the validator.validate returns true even if the call to json api returns false.
Here's the code (I use the Kendo MVC helpers but it's not related to that the problem)
<div id="divValidator1">
<div class="form-group">
#Html.LabelFor(m => m.Email)
#(Html.Kendo().TextBoxFor(m => m.Email)
.HtmlAttributes(new { placeholder = "you#domain.com", type = "email", #class = "k-textbox required",data_bind="value: XXX" })
)
</div>
<footer class="col-xs-12 form-group text-right">
#(Html.Kendo().Button()
.Name("Next1")
.Content("Next")
.HtmlAttributes(new { #class = "k-primary", data_bind = "enabled: isEnabled" })
.Events(ev => ev.Click("onNextClick")))
</footer>
var kendoValid = $("#divValidator1").kendoValidator({
validateOnBlur: false,
rules: {
remote: function (input) {
if (input.val() == "" || !input.attr("data-val-remote-url")) {
return true;
}
if (input.attr("data-val-remote-recieved")) {
input.attr("data-val-remote-recieved", "");
return !(input.attr("data-val-remote"));
}
var url = input.attr("data-val-remote-url");
var postData = {};
postData[input.attr("data-val-remote-additionalfields").split(".")[1]] = input.val();
var validator = this;
var currentInput = input;
input.attr("data-val-remote-requested", true);
$.ajax({
url: url,
type: "POST",
data: JSON.stringify(postData),
dataType: "json",
traditional: true,
async:false,
contentType: "application/json; charset=utf-8",
success: function (data) {
if (data == true) {
input.attr("data-val-remote", "");
}
else {
input.attr("data-val-remote", false);
}
input.attr("data-val-remote-recieved", true);
// validator.validateInput(currentInput);
},
error: function () {
input.attr("data-val-remote-recieved", true);
validator.validateInput(currentInput);
}
});
return true;
}
},
messages: {
remote: function (input) {
return input.attr("data-val-remote");
}
}
}).data("kendoValidator");
var viewModel1 = kendo.observable({
XXX:null,
isEnabled: function () {
var self = this;
self.get("XXX");
var x = $("#divValidator1").data("kendoValidator");
console.log(x.validate());
}
});
kendo.bind($("#divValidator1"), viewModel1);
and here's the controller
public ActionResult IsEmailJustPresent(string email)
{
var res = email == "something";
return Json(res, JsonRequestBehavior.AllowGet);
}
And the ViewModel
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email")]
[Remote("IsEmailJustPresent", "Home")]
public string Email { get; set; }
}
I've also used a kendo.observable so that I can enable/disable the next button based on the validation value.
In the isEnabled function you see I do var x = $("#divValidator1").data("kendoValidator"); but it returns always true (seems to only check for the required attribute).
To reach that solution I started from here
If you want there's a more complex example I've created on git that includes the wizard I'll need in my final project

Could this commented line be the problem?
// validator.validateInput(currentInput);
Is the attribute "data-val-remote" correctly set?

Related

JQuery ajax call blocks RedirectToAction

I have a view with an ajax call:
$.ajax({
url: "CreateChecklistCopies",
type: "POST",
data: JSON.stringify(drivers),
async: false,
contentType: "application/json; charset=utf-8",
});
The controller action performs some tasks and redirects to the index method of the controller:
[HttpPost]
public IActionResult CreateChecklistCopies([FromBody] object i_vm)
{
var tmp = Newtonsoft.Json.JsonConvert.DeserializeObject<List<ChecklistCopyModel>>(i_vm.ToString());
int result = _obj.AddChecklistCopies(tmp);
if (result > 0)
return RedirectToAction("Index", new { SuccessMessage = "Checklists were successfully duplicated." });
else
return RedirectToAction("Index", new { ErrorMessage = "An error occurred when duplicating the checklist." });
}
The Index action is successfully executed but there's no forward to the index page happening:
[HttpGet]
public IActionResult Index(string FilterCreator, string salesPersonFilter, string SuccessMessage, string ErrorMessage)
{
if (FilterCreator == null)
{
FilterCreator = User.Identity.Name.Split("\\")[1];
}
else if (FilterCreator.ToLower() == "all")
{
FilterCreator = null;
}
var checklists = _obj.GetChecklists(true, FilterCreator, salesPersonFilter);
var salespersons = _obj.GetSalespersons();
var chlVm = _mapper.Map<List<ChecklistModel>, List<ChecklistListViewModel>>(checklists);
var ivm = new IndexViewModel
{
CheckLists = chlVm,
Salespersons = salespersons,
SuccessMessage = !string.IsNullOrEmpty(SuccessMessage) ? SuccessMessage : "",
ErrorMessage = !string.IsNullOrEmpty(ErrorMessage) ? ErrorMessage : ""
};
return View(ivm);
}
I played around with the async: false tag in the ajax call but that didn't help. Any ideas?
You cannot use RedirectToAction to action in an ajax call to redirect the entire page. Because the ajax response is limited to the ajax request scope only.
What you can do is return a json object instead of RedirectToAction like this:
[HttpPost]
public IActionResult CreateChecklistCopies([FromBody] object i_vm)
{
var tmp = Newtonsoft.Json.JsonConvert.DeserializeObject<List<ChecklistCopyModel>>(i_vm.ToString());
int result = _obj.AddChecklistCopies(tmp);
JsonResult result = new JsonResult(new JsonSerializerSettings());
if (result > 0)
result = Json(new { IsRedirect = True, RedirectUrl = '/controller/Index/...', SuccessMessage = "Checklists were successfully duplicated." });
else
result = Json(new { IsRedirect = True, RedirectUrl = '/controller/Index/...', SuccessMessage = "An error occurred when duplicating the checklist." });
return result;
}
Then in the ajax call do this:
$.ajax({
url: "CreateChecklistCopies",
type: "POST",
data: JSON.stringify(drivers),
dataType: 'JSON',
async: false,
}).done(function (response) {
if (response != null) {
window.location = response.RedirectUrl;
//You can also use the IsRedirect and SuccessMessage property as needed
} else {
alert('There was a problem processing the request.');
}
}).fail(function () {
alert('There was a problem processing the request.');
});

Remote validation for kendowindow

I implement remote validation. It works for usual fields correctly, but if field is situated in kendowindow jquery validation does not work.
How can I solve this problem?
This can be achieved using Kendo UI validator as demonstrated below:
Model with "Remote" annotation attribute:
public class ProductViewModel
{
[Editable(false)]
public int ProductID { get; set; }
[Required]
[Remote("UniqueName", "Home", ErrorMessage = "The entered name already exists.")]
public string ProductName { get; set; }
}
Controller with "UniqueName" Action:
public ActionResult UniqueName(string productName)
{
var context = new NorthwindEntities();
return Json(!context.Products.Any(p => p.ProductName == productName), JsonRequestBehavior.AllowGet);
}
Script to add custom validation rule to the Kendo UI validation rules for the "Remote" validation attribute (can be placed anywhere on the page before Grid initialization code):
<script>
(function ($, kendo) {
$.extend(true, kendo.ui.validator, {
rules: {
//define custom validation rule to match remote validation:
mvcremotevalidation: function (input) {
if (input.is("[data-val-remote]") && input.val() != "") {
var remoteURL = input.attr("data-val-remote-url");
var valid;
$.ajax({
async: false,
url: remoteURL,
type: "GET",
dataType: "json",
data: validationData(input, this.element),
success: function (result) {
valid = result;
},
error: function () {
valid = false;
}
});
return valid;
}
return true;
}
},
messages: {
mvcremotevalidation: function (input) {
return input.attr("data-val-remote");
}
}
});
function validationData(input, context) {
var fields = input.attr("data-val-remote-additionalFields").split(",");
var name = input.prop("name");
var prefix = name.substr(0, name.lastIndexOf(".") + 1);
var fieldName;
var data = {};
for (var i = 0; i < fields.length; i++) {
fieldName = fields[i].replace("*.", prefix);
data[fieldName] = $("[name='" + fieldName + "']", context).val();
}
return data;
}
})(jQuery, kendo);
</script>
Grid initialization code:
#(Html.Kendo().Grid<KendoUIMVC5.Models.ProductViewModel>()
.Name("grid")
.Columns(columns =>
{
columns.Command(comm =>
{
comm.Edit();
});
columns.Bound(p => p.ProductID);
columns.Bound(p => p.ProductName);
})
.Pageable()
.Sortable()
.Filterable()
.DataSource(dataSource => dataSource
.Ajax()
.Model(model =>
{
model.Id(p => p.ProductID);
})
.Read(read => read.Action("Read", "Home"))
.Update(update => update.Action("Update", "Home"))
)
)

Image Upload using MVC API POST Multipart form

My View :-
<form class="commentform commentright" enctype="multipart/form-data" >
<textarea class="simpleta required" name="Body" placeholder="Discuss this vehicle with other members of Row52."></textarea>
<input type="file" id="fileuploadfield" name="fileuploadfield"/>
<input type="submit" value="Submit" class="btn btn-inverse btn-small"/>
<input type="hidden" name="ParentId" value="0"/>
<input type="hidden" name="VehicleId" value="#Model.VehicleId"/>
<span class="commentcount"><span class="numberofcomments">#Model.Count</span>#count</span>
<div class="feedback"></div>
<img id="img" src="" />
</form>
JQuery :-
submitComment: function (form, type) {
var self = this;
var $this = $(form);
var formData = $this.serialize();
var $message = $this.find('.feedback');
$message.hide();
$message.html('');
var val = validation({ $form: $this });
if (val.checkRequired()) {
$this.indicator({ autoStart: false, minDuration: 100 });
$this.indicator('start');
var files = $("#fileuploadfield").get(0).files;
if (files.length > 0) {
if (window.FormData !== undefined) {
var data = new FormData();
for (var i = 0; i < files.length; i++) {
data.append("file" + i, files[i]);
}
}
else {
alert("This browser doesn't support HTML5 multiple file uploads!");
}
}
else {
alert("This");
}
$.ajax('/api/v2/comment/Post/', {
type: 'POST',
contentType: 'multipart/form-data',
// I have also use contentType: false,
processData: false,
data: formData
}).done(function (d) {
Controller :-
public Task<HttpResponseMessage> Post()
{
var provider = new MultipartMemoryStreamProvider();
var task1 = Request.Content.ReadAsMultipartAsync(provider);
var userId = User.Id;
return task1.Then(providerResult =>
{
var file = GetFileContent(providerResult);
var task2 = file.ReadAsStreamAsync();
string originalFileName = file.Headers.ContentDisposition.FileName.Replace("\"", "");
string extension = Path.GetExtension(originalFileName);
string fileName = Guid.NewGuid().ToString() + extension;
return task2.Then(stream =>
{
if (stream.Length > 0)
{
var kernel = WindsorContainerFactory.Create(KernelState.Thread, ConfigurationState.Web);
CloudBlobContainer container = GetContainer();
var userRepo = kernel.Resolve<IUserRepository>();
var logger = kernel.Resolve<ILogger>();
logger.Fatal("Original File Name: " + originalFileName);
logger.Fatal("Extension: " + extension);
logger.Fatal("File Name: " + fileName);
var user = userRepo.FirstOrDefault(x => x.Id == userId);
var path = CreateImageSize(stream, 500, fileName, userId, container, logger);
if (user != null)
{
user.AvatarOriginalFileName = fileName;
user.AvatarOriginalAbsolutePath =
ConfigurationManager.AppSettings["CdnUserEndpointUrl"] + path;
user.AvatarOriginalRelativePath = path;
user.AvatarCroppedAbsolutePath = "";
user.AvatarCroppedFileName = "";
user.AvatarCroppedRelativePath = "";
userRepo.Update(user);
}
else
{
Logger.Error("User is null. Id: " + userId.ToString());
}
kernel.Release(userRepo);
kernel.Release(logger);
kernel.Dispose();
}
var response = Request.CreateResponse(HttpStatusCode.Moved);
response.Headers.Location = new Uri("/Account/Avatar", UriKind.Relative);
return response;
});
});
}
Following Error get :-
"Invalid 'HttpContent' instance provided. It does not have a content type header starting with 'multipart/'. Parameter name: content"
There's no standard Task.Then method in .NET framework, and I couldn't find any implementation details in the code you posted. I'd assume it was taken from here:
Processing Sequences of Asynchronous Operations with Tasks
If that's the case, you may be loosing AspNetSynchronizationContext context inside your Task.Then lambdas, because the continuation may happen on a different ASP.NET pool thread without proper synchronization context. That would explain why HttpContent becomes invalid.
Try changing the implementation of Task.Then like this:
public static Task<T2> Then<T1, T2>(this Task<T1> first, Func<T1, Task<T2>> next)
{
if (first == null) throw new ArgumentNullException("first");
if (next == null) throw new ArgumentNullException("next");
var tcs = new TaskCompletionSource<T2>();
first.ContinueWith(delegate
{
if (first.IsFaulted) tcs.TrySetException(first.Exception.InnerExceptions);
else if (first.IsCanceled) tcs.TrySetCanceled();
else
{
try
{
var t = next(first.Result);
if (t == null) tcs.TrySetCanceled();
else t.ContinueWith(delegate
{
if (t.IsFaulted) tcs.TrySetException(t.Exception.InnerExceptions);
else if (t.IsCanceled) tcs.TrySetCanceled();
else tcs.TrySetResult(t.Result);
}, TaskScheduler.FromCurrentSynchronizationContext());
}
catch (Exception exc) { tcs.TrySetException(exc); }
}
}, TaskScheduler.FromCurrentSynchronizationContext());
return tcs.Task;
}
Note how the continuations are now scheduled using TaskScheduler.FromCurrentSynchronizationContext().
Ideally, if you can use .NET 4.5, you should be using async/await: Using Asynchronous Methods in ASP.NET 4.5. This way, the thread's synchronization context is automatically captured for await continuations.
The following may facility porting:
Implementing Then with Await.

Calling a server side JsonResult method from JavaScript

I need to call the following JsonResult method:
JsonResult Delete(int pubId)
{
try
{
using (var ctx = new LibsysLiteContext())
{
var p = ctx.Publishers.Find(pubId);
var allPublisher = ctx.Publishers.ToList();
ctx.Publishers.Remove(p);
var total = allPublisher.Count();
return Json(new { success = true, data = allPublisher, total = total }, JsonRequestBehavior.AllowGet);
}
return Json(new RestResult { Success = true, Data = entity, Message = "Country has been deleted" }, JsonRequestBehavior.DenyGet);
return null;
}
catch (Exception e)
{
return Json(new RestResult { Success = true, Message = e.Message }, JsonRequestBehavior.DenyGet);
}
}
from a js function (deleteRows):
var deleteRows = function () {
Ext.Msg.confirm(
'Delete Rows', 'Are you sure?',
function(btn) {
if (btn == 'yes') {
var hh = Ext.getCmp('gg').deleteSelected();
ajax({
//action and controller
url: '#Url.Action( "Publisher", "Delete")',
data: { "Id": Id },
type: 'POST',
dataType: 'json',
});
}
});
};
which is called by a handler of the following button:
X.Button().ID("bntdelete").Text("delete").Icon(Icon.Delete).Handler("deleteRows();"),
It didn't work at all this way! How can I move from client side to the server side from a JavaScript function?
In general calling a serverside [Direct Method] from js you use App.direct.<Method>();
Hopefully you have resolved this by now but in your ajax call you are defining the type as post but what you show on the controller is set as the default get. you need to add
[HttpPost]
public JsonResult Delete(int id)...
to your controller

How to update Chosen dropdownlist's items(cascade dropdownlist)?

I use Chosen plugin. I want to refresh Category (Chosen) dropdownlist, when change Section dropdownlist.
Here is view:
#model CategoryInputModel
#Html.DropDownListFor(model => model.SectionId, ViewBag.SectionList as IEnumerable<SelectListItem>, new { id = "SectionId" })
#Html.ListBoxFor(model => model.CategoryIdSet, ViewBag.AllCategoryList as MultiSelectList
, new
{
#class = "chzn-select",
data_placeholder = "Choose Categories...",
#style = "width : 500px;",
id = "CategoryIdSet"
})
CategoryInputModel is like:
public class CategoryInputModel
{
public string Name { get; set; }
public int SectionId{ get; set; }
public List<int> CategoryIdSet{ get; set; }
}
I can create cascade dropdownlist for simple lists, but could not create it for multiple select. I tried this :
<script type="text/javascript">
$(function () {
$("#SectionId").change(
function () {
loadLevelTwo(this);
});
loadLevelTwo($("#SectionId"));
});
function loadLevelTwo(selectList) {
var selectedId = $(selectList).val();
$.ajax(
{
url: "#Url.Action("GetCategoriesBySectionId", "Project")",
type: "GET",
data: { id: selectedId },
success: function (data) {
$("#CategoryIdSet").html($(data).html());
},
error: function (xhr) {
alert("Something went wrong, please try again");
}
});
}
</script>
In controller:
public ActionResult GetCategoriesBySectionId(int id)
{
var result = MyService.GetCategoriesBySectionId(id);
// **its problem to create partial view that return chosen dropdownlist I need**
}
How can I create cascade Chosen dropdownlist?
I think you need to add a little more to your ajax callback. I replaced success method with done. Try this, it works for me:
function loadLevelTwo(selectList) {
var selectedId = $(selectList).val();
$.ajax(
{
url: "#Url.Action("GetCategoriesBySectionId", "Project")",
type: "GET",
data: { id: selectedId },
error: function (xhr) {
alert("Something went wrong, please try again");
}
}).done(function (data) {
$("#CategoryIdSet").children().each(function (index, option) {
$(option).remove();
});
//blank option
var items = "<option selected value=\"\"></option>";
$.each(data,
function () {
items += "<option value=\"" + this[0] + "\">" + this[1] + "</option>";
});
$("#CategoryIdSet").html(items)
$("#CategoryIdSet").trigger("liszt:updated");
$("#CategoryIdSet").change();
});
}
controller action could look like this:
public ActionResult GetCategoriesBySectionId(int id)
{
var result = MyService.GetCategoriesBySectionId(id);
//build JSON.
var modelDataStore = from m in result
select new[] { m.YourValueProperty.ToString(),
m.YourTextProperty };
return Json(modelDataStore, JsonRequestBehavior.AllowGet);
}

Resources