I can't get the ObjectId for a model to return back with a value using the MongoDB csharp official library. When I submit the form that posts back to the controller, the PageID always comes back {000000000000000000000000} in the model. The HTML rendered has the valid id, that one field never comes back properly from the form post.
The html renders: <input id="PageID" name="PageID" type="hidden" value="4f83b5ccd79f660b881887a3" />
This is what I have.
Model:
public class PageModel
{
public PageModel()
{
CanEdit = false;
CancelRequest = false;
}
[BsonId]
public ObjectId PageID { get; set; }
[Display(Name = "Page URL")]
public string PageURL { get; set; }
[AllowHtml]
[Display(Name = "Content")]
public string Value { get; set; }
[Display(Name = "Last Modified")]
public DateTime LastModified { get; set; }
[BsonIgnore]
public bool CancelRequest { get; set; }
[BsonIgnore]
public bool CanEdit { get; set; }
}
Controller Post:
[HttpPost]
public ActionResult Edit(PageModel model)
{
// check to see if the user canceled
if (model.CancelRequest)
{
return Redirect(model.PageURL);
}
// check for a script tag to prevent
if (!model.Value.Contains("<script"))
{
// save to the database
model.LastModified = DateTime.Now;
collection.Save(model);
// return to the page
return Redirect(model.PageURL);
}
else
{
ModelState.AddModelError("Value", "Disclosures discovered a script in the html you submitted, please remove the script before saving.");
}
return View(model);
}
View:
#model LeulyHome.Models.PageModel
#{
ViewBag.Title = "";
Layout = "~/Views/Shared/_Layout.cshtml";
}
#using (Html.BeginForm())
{
<fieldset>
<legend>Edit Page</legend>
<div class="control-group">
#Html.LabelFor(m => m.PageURL, new { #class = "control-label" })
<div class="controls">
#Html.TextBoxFor(m => m.PageURL, new { #class = "input-xlarge leu-required span9" })
#Html.ValidationMessageFor(m => m.PageURL, null, new { #class = "help-block" })
</div>
</div>
<div class="control-group">
#Html.LabelFor(m => m.Value, new { #class = "control-label" })
<div class="controls">
#Html.TextAreaFor(m => m.Value)
#Html.ValidationMessageFor(m => m.Value, null, new { #class = "help-block" })
</div>
</div>
<div class="control-group">
#Html.LabelFor(m => m.LastModified, new { #class = "control-label" })
<div class="controls">
#Html.DisplayFor(m => m.LastModified)
#Html.HiddenFor(m => m.LastModified)
</div>
</div>
#Html.HiddenFor(m => m.PageID)
#Html.HiddenFor(m => m.CancelRequest)
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save Page</button>
<button type="button" class="btn btn-danger" id="cancelBtn">Cancel Changes</button>
</div>
</fieldset>
}
#section Footer {
<script type="text/javascript" src="#Url.Content("~/Content/ckeditor/ckeditor.js")"></script>
<script language="javascript" type="text/javascript">
$(document).ready(function () {
// adjust the editor's toolbar
CKEDITOR.replace('Value', {
toolbar: [["Bold", "Italic", "Underline", "-", "NumberedList", "BulletedList", "-", "Link", "Unlink"],
["JustifyLeft", "JustifyCenter", "JustifyRight", "JustifyBlock"],
["Cut", "Copy", "Paste", "PasteText", "PasteFromWord", "-", "Print", "SpellChecker", "Scayt"], ["Source", "About"]]
});
$("#cancelBtn").click(function () {
$("#CancelRequest").val("True");
$("#updateBtn").click();
});
});
</script>
}
It looks like you are sending a string value for PageID which you are expecting an object of type ObjectId.
The model binding isn't going to know how to take this string and turn it into an ObjectId. If you take a look at the ObjectId class in the MongoDriver you will see that it is pretty hairy.
We use mongodb quite a bit and for our id's we simply use strings which provide a lot of flexibility. I am not sure the use case for which you need the ObjectId class as your document Id but you may not need it.
So from here it seems like you have two options.
Change your document id's to strings or something else
Write a custom model binder for the ObjectId class
Hope that helps :)
Create Binding:
public class ObjectIdBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var result = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
return !string.IsNullOrWhiteSpace(result.AttemptedValue) ? ObjectId.Parse(result.AttemptedValue) : ObjectId.Empty;
}
}
Create ModelBinderConfig:
namespace Nasa8x.CMS
{
public class ModelBinderConfig
{
public static void Register(ModelBinderDictionary binder)
{
binder.Add(typeof(ObjectId), new ObjectIdBinder());
binder.Add(typeof(string[]), new StringArrayBinder());
binder.Add(typeof(int[]), new IntArrayBinder());
}
}
}
Register on Global.cs:
protected void Application_Start()
{
ModelBinderConfig.Register(ModelBinders.Binders);
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
If you don't need the PageID property in your C# Class to be of Type ObjectId, but want to take advantage of this Type on the MongoDB-Side, you can let the Driver take care of the conversion in your class-Definition:
public class PageModel
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string PageID { get; set; }
}
Related
I am creating an application where a Note can be created and one to many Parts can be added to the note. (The application is for a tractor salvage yard where customers call for tractor parts). I know similar questions have been asked before. But I couldn't find anything very relevant to my situation with EF and all.
I am having a lot of difficulty with creating/editing a Note with its Parts in one view. I want to focus on editing for this question, though.
I have two simple CLR classes with a relation.
public class Note
{
public int ID { get; set; }
public string CustomerName { get; set; }
public string CustomerPhone { get; set; }
public DateTime DateCreated { get; set; }
public DateTime DateUpdated { get; set; }
public string CreatedBy { get; set; }
public string AssignedTo { get; set; }
public virtual ICollection<Part> Parts { get; set; }
}
public class Part
{
public int PartID { get; set; }
public string PartNumber { get; set; }
public string Description { get; set; }
public int NoteID { get; set; }
public virtual Note Note { get; set; }
}
And the DbContext:
public class CallNoteContext : DbContext
{
public CallNoteContext() { }
public DbSet<Note> Notes { get; set; }
public DbSet<Part> Parts { get; set; }
}
My problem is binding the data from both entities to the edit view, accessing the data in the view for editing and saving the note and and multiple parts to the database in the httppost action.
I have tried a lot of things, but after reading a lot of articles, I keep coming back to this for the controller and view. To me it seems like this should work. But obviously I am missing something.
Here is the edit and post actions from my controller.
private CallNoteContext db = new CallNoteContext();
// GET: Note/Edit/5
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Note note = db.Notes.Find(id);
var model = new Note()
{
CustomerName = note.CustomerName,
CustomerPhone = note.CustomerPhone,
DateCreated = note.DateCreated,
DateUpdated = note.DateUpdated,
CreatedBy = note.CreatedBy,
AssignedTo = note.AssignedTo,
Parts = note.Parts
};
if (note == null)
{
return HttpNotFound();
}
return View(model);
}
// POST: Note/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "ID,CustomerName,CustomerPhone,DateCreated,DateUpdated,CreatedBy,AssignedTo,Parts")] Note note)
{
if (ModelState.IsValid)
{
foreach(var p in note.Parts)
{
db.Entry(p).State = EntityState.Modified;
db.SaveChanges();
}
db.Entry(note).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(note);
}
When I try to make editors for p.PartNumber and p.Description in my view below, it breaks with the exception that it can't find these properties. I have a feeling that I am doing something wrong in the "get" action of the controller. But I am having a hard time figuring out what is wrong.
By the way, IntelliSense is saying No Issues Found for the controller.
Here is my Edit view.
#model CallNote.Models.Note
<head>
<script src="~/Scripts/jquery-3.4.1.js" type="text/javascript"></script>
</head>
<h2>Edit</h2>
#using (Html.BeginForm())
{
#Html.HiddenFor(model => model.ID)
<div class="form-group">
#Html.LabelFor(model => model.CustomerName, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.CustomerName, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.CustomerName, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.CustomerPhone, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.CustomerPhone, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.CustomerPhone, "", new { #class = "text-danger" })
</div>
</div>
#*There are editors here for all of the properties, but I didn't list them to save space.*#
#*The app always breaks when it gets to this foreach because it says it can't find p.PartNumber. What is wrong?
#foreach (var p in Model.Parts)
{
<div>
#*I also tried just using p.PartNumber, but it says p doesn't exist in current context.
#Html.EditorFor(p => p.PartNumber)
#Html.EditorFor(p => p.Description)
</div>
}
<div id="partInfo" style="display:none">
#Html.EditorFor(p => p.PartNumber)
#Html.EditorFor(p => p.Description)
</div>
<div id="btnWrapper">
<input id="btnAddPart" type="button" value="Add Part" />
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
#*The script below works, allowing you to add part editors*#
<div>
#Html.ActionLink("Back to List", "Index")
</div>
<script>
$(document).ready(function () {
$("#btnAddPart").click(function () {
var partinfo = $("#partInfo").html();
$("#partInfo").append(partinfo);
});
});
</script>
Also I am unsure if the httppost action will work. I have not been able to try it yet as I cannot get the Edit view to even load yet. So if you have any suggestions for that too, let me know.
I am just getting started with MVC, so a detailed answer would be super!
you have to include Parts in the Note
....
Note note = db.Notes
.Include(i=> i.Parts)
.FirstOrDefault(i=>i.ID==id);
if (note == null)
{
return HttpNotFound();
}
.....
and since you are using editor, replace foreach loop by for loop
#if( Model.Parts!=null && Model.Parts.Count >0)
{
#for (var i=0; i< Model.Parts.Count; i++)
{
<div id="partInfo" style="display:none">
#Html.EditorFor(model => model.Parts[i].PartNumber)
#Html.EditorFor(model => model.Parts[i].Description)
</div>
...... and so on for all properties
}
}
and remove bind from the action
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(Note note)
{
if (!ModelState.IsValid) return View(note);
var existedNote = db.Notes
.Include(i=> i.Parts)
.FirstOrDefault(i=>i.ID==note.ID);
if(existedNote!=null)
{
db.Entry(existedNote).CurrentValues.SetValues(note);
if(note.Parts!=null && note.Parts.Count > 0)
{
foreach( var part in note.Parts)
{
var existingPart = existingNote.Parts.FirstOrDefault(p => p.PartID == part.PartID);
if (existingPart == null)
{
existingNote.Parts.Add(part);
}
else
{
context.Entry(existingPart).CurrentValues.SetValues(part);
}
}
}
db.SaveChanges();
}
return RedirectToAction("Index");
}
return View(note);
}
````
In my form, which I created in a view, the user can press add or search.
If the "add" button is pressed, a different model should be used in the background than with the "search" option. The add model is validated but otherwise does not differ from the search model.
By clicking "search" the user shouldn't be forced to fill in all fields.
Code
Model - AddModel
[Key]
public int Id { get; set; }
[Required]
[Display(Name = "Name")]
[StringLength(200, MinimumLength = 1, ErrorMessage = "Not Allowed")]
public string Name { get; set; }
[Required]
[Display(Name = "Place")]
[RegularExpression(#"^[\w ]*$", ErrorMessage = "Not Allowed")]
public string Place { get; set; }
Model - SearchModel
public int Id { get; set; }
public string Name { get; set; }
public string Place{ get; set; }
Controller
[HttpPost, ValidateAntiForgeryToken]
public IActionResult Add(AddModel p) {
if (ModelState.IsValid) {
_ = InsertData(p);
ModelState.Clear();
return RedirectToAction("Add", new { Success = true });
}
return View();
}
public IActionResult Select(SearchModel p)
{
Task.WaitAll(SelectData(p));
return View(per); // per => list of selected data
}
View
#model **AddModel**
#if (ViewBag.success)
{
...
}
<form method="POST">
<div class="form-group">
#Html.LabelFor(m => m.Name, new { })
#Html.EditorFor(m => m.Name, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(m => m.Name, "", new { #class = "text-danger" })
</div>
<div class="form-group">
#Html.LabelFor(m => m.Place, new { })
#Html.EditorFor(m => m.Place, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(m => m.Place, "", new { #class = "text-danger" })
</div>
<input asp-action="Add" type="submit" class="btn btn-outline-primary" value="Add" />
<input asp-action="Select" type="submit" class="btn btn-outline-success" value="Search" />
</form>
The AddModel is still used in the View, but I would like to specify in the controller which model I would like to use. So if you press "search" the SearchModel and with "add" the AddModel should be used. I've already tried it with dynamic, but then it came to problems with the #html helpers.
Does somebody has any idea?
Would appreciate ;)
I think what you are looking to do is called a ViewModel, this should help : https://learn.microsoft.com/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-3
I am using Multi-Select text box in my view..
Below is my code
View:
#using (Ajax.BeginForm("Multiple", "Home", new AjaxOptions { HttpMethod = "POST" }))
{
<div class="col-md-2 col-xs-2">Associated Segment</div>
<div class="col-md-4 col-xs-4">
#Html.ListBoxFor(model => model.SegmentList, new MultiSelectList(ViewBag.List, "Value", "Text", Model.SegmentList.AsEnumerable()), new { #class = "form-control tokenizationSelect2", #multiple = "multiple" })
</div>
<div class="col-md-12 col-xs-12">
<div class="pull-right" style="padding:1%;">
<button class="btn btn-sm btn-primary" id="btnSaveProbDiagnosis" type="submit" name="">save</button>
</div>
</div>
}
<link href="~/scripts/select2.min.css" rel="stylesheet" />
<script src="~/scripts/jquery-1.10.2.js"></script>
<script src="~/scripts/select2.js"></script>
<script src="~/scripts/select2.full.js"></script>
<script type="text/javascript">
$(document).ready(function () {
$(".tokenizationSelect2").select2({
placeholder: "Your favourite car", //placeholder
tags: true,
tokenSeparators: ['/', ',', ';', " "]
});
})
</script>
Controller:
[HttpPost]
public ActionResult Multiple(ViewModal viewmoddal)
{
return View();
}
Modal:
public class ViewModal
{
public int QuestionId { get; set; }
public string question { get; set; }
public string type { get; set; }
public List<SegmentList> SegmentList { get; set; }
public List<Answer> option { get; set; }
}
Its perfectly binding data in the UI. But when I am posting the data to controller
data not going to controller.
someone please help me in this...
You need to define array of integers to bind to multi select and these will be posted to controller actions. for.e.g in your case define your model like this.
public class ViewModal
{
public int[] SelectedSegments { get; set; }
public List<Segment> SegmentList { get; set; }
}
and in view
#Html.ListBoxFor(model => model.SelectedSegments, new MultiSelectList(ViewBag.List, "Value", "Text", Model.SegmentList.AsEnumerable()), new { #class = "form-control tokenizationSelect2", #multiple = "multiple" })
My attempt at getting JQueryValidate custom validation attributes does not seem to be working when using jquery.load. This is my code:
CreateViewModel.cs
public class CreateViewModel
{
[DisplayName("Artwork")]
public int ArtworkId { get; set; }
[AtLeastOne("selectiongroup")]
public bool IsPromo { get; set; }
[AtLeastOne("selectiongroup")]
public bool IsUpc { get; set; }
[AtLeastOne("selectiongroup")]
public bool IsCoupon { get; set; }
public SelectList ArtworkSelectList { get; set; }
}
AtLeastOneAttribute.cs
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class AtLeastOneAttribute : ValidationAttribute, IClientValidatable
{
public string GroupName { get; private set; }
public AtLeastOneAttribute(string groupName)
{
GroupName = groupName;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
yield return new ModelClientValidationRule
{
ErrorMessage = "At least one of Coupon or Upc or Promo is required.",
ValidationType = "atleastone",
ValidationParameters =
{
new KeyValuePair<string, object>("groupname", GroupName),
}
};
}
}
Page.cshtml
<form method="POST" id="signCreationForm">
<div class="container-fluid">
<div class="row">
<div class="col-lg-6">
#Html.LabelFor(model => model.ArtworkId)
#Html.DropDownListFor(model => model.ArtworkId, Model.ArtworkSelectList, "Select an Artwork")
</div>
</div>
<div class="row">
<div class="col-lg-6"></div>
<div>#Html.ValidationMessageFor(model => model.ArtworkId)</div>
</div>
<div class="row">
<div class="col-lg-10">
<label>Associated With:</label>
</div>
</div>
<div class="row">
<div class="col-lg-4">
#Html.LabelFor(model => model.IsUpc, "UPC")
#Html.CheckBoxFor(model => model.IsUpc)
#Html.ValidationMessageFor(model => model.IsUpc)
</div>
<div class="col-lg-4">
#Html.LabelFor(model => model.IsCoupon, "Coupon")
#Html.CheckBoxFor(model => model.IsCoupon)
#Html.ValidationMessageFor(model => model.IsCoupon)
</div>
<div class="col-lg-4">
#Html.LabelFor(model => model.IsPromo, "Promotion")
#Html.CheckBoxFor(model => model.IsPromo)
#Html.ValidationMessageFor(model => model.IsPromo)
</div>
</div>
</div>
</form>
#Scripts.Render("~/bundles/jqueryval")
<script type="text/javascript">
$.validator.addMethod("atleastone", function (value, element, params) {
return $("[data-val-atleastone-groupname="+params+"]:checked").length > 0;
});
$.validator.unobtrusive.adapters.add("atleastone", ["groupname"], function (options) {
options.rules["atleastone"] = options.params.groupname;
options.messages["atleastone"] = options.message;
});
</script>
CallingPage.cshtml (javascript that is calling it)
$("#create-ad[data-link]").on("click", function() {
var link = $(this).data("link");
$createDialog.load(link, function() {
$createDialog.dialog("open");
$.validator.unobtrusive.parse($("#signCreationForm"));
});
});
Now, this code works in a separate project. In this project, the partial view is loaded via jquery.load() event and for some reason this causes the custom method to never execute. I know this because the alerts would normally fire off once for each attribute decorated with atleastone attribute.
Is there anything special that needs to be done on Page.cshtml when using jquery load with custom jqueryvalidate validations?
Please put your custom method in to jquery document ready;;;;;;
For I.E.
<script type="text/javascript">
$(document).ready(function(){
$.validator.addMethod("atleastone", function (value, element, params) {
alert("halp");
return $(".grouped:checked").length > 0;
});
$.validator.unobtrusive.adapters.add("atleastone", ["groupname"], function (options) {
alert("halp");
options.rules["atleastone"] = "#" + options.params.groupname;
options.messages["atleastone"] = options.message;
});
});
</script>
The suggested duplicate for this scenario answer https://stackoverflow.com/a/16105954/82333 got the first half of the problem which was that the the add $.validator.unobtrusive.adapters.add was not functioning. However, after that the rest of the code wasn't running either.
#Bhavin Solanki's comment to include the validation files on the calling page fixed the other half, which was that on submit the code was not running.
I have some problem posting a form with 'complex type' model:
I have a Model:
public class CircleEditViewModel
{
[Key]
public int CircleId { get; set; }
[Required]
[MaxLength(100)]
public string Name { get; set; }
public bool IsSystem { get; set; }
public class UserInCircle
{
public UserInCircle(User user)
{
this.UserId = user.UserId;
FullName = user.FullName;
}
public int UserId { get; set; }
public byte[] Picture { get; set; }
public string FullName { get; set; }
public bool isInCircle { get; set; }
}
public List<UserInCircle> Users { get; set; }
}
My first problem was that at post event, my Users where null.. so i followed a few posts on here (like MVC- Model Binding on a Complex Type) to use a for instead of a foreach,but since i did so, my form won't post anymore:
View:
#model Wims.Website.ViewModels.CircleEditViewModel
<script type="text/javascript">
$(document).ready(function () {
$.validator.unobtrusive.parse('form');
});
</script>
#using (Ajax.BeginForm(Html.ViewContext.RouteData.Values["Action"].ToString(), null, new AjaxOptions { HttpMethod = "POST", OnSuccess = "SaveDone(data)" }, new { id = "editform" }))
{
#Html.ValidationSummary(true)
<fieldset>
<legend>Circle</legend>
#Html.Label(DateTime.Now.ToString());
<div class="editor-label">
#Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Name)
#Html.ValidationMessageFor(model => model.Name)
</div>
</fieldset>
if (Model.Users != null)
{
for (int i = 0; i < Model.Users.Count; i++)
{
<div class="userDetail">
<div>
<div>
#Html.DisplayFor(model => Model.Users[i].isInCircle);
</div>
<div class="iconDiv">
#Html.Image("~/Content/Images/defaultUser.jpg", Model.Users[i].FullName, null);
</div>
<div>
#Html.TextBoxFor(model => Model.Users[i].FullName)
#Html.HiddenFor(model => Model.Users[i].UserId)
</div>
</div>
</div>
<div style="clear: both"></div>
}
}
#Html.GenerateSecureDataControls(model => model.CircleId)
<input type="submit" value="Save" />
}
My view is rendered as a partial loaded thru ajax (not sure it makes any difference here).
Any idea why it won't post? If i remove all the '[]' like 'Users[0].FullName' to Users0.FullName i will post, but of course it won't be bound.
Thanks for your help
Edit just in case needed: Action:
[HttpPost]
public ActionResult Edit(CircleEditViewModel circleData, FormCollection collection)
{
if (ModelState.IsValid)
{
using (var logic = new CircleLogic())
{
Circle circle = logic.GetCircleById(circleData.CircleId, WebMatrix.WebData.WebSecurity.CurrentUserId);
if (circle == null)
{
return HttpNotFound();
}
else
{
circle.Name = circleData.Name;
logic.UpdateCircle(circle, GetSelectedUser(collection));
}
return PartialView("_CircleAndUsers", GetData(logic, circle.CircleId));
}
}
return this.Json(new { success = false, viewdata = RenderRazorViewToString("_CircleAndUsers", circleData) });
}
Pablo Romeo was right, i added a default ctor and it worked.