In my MVC Controller, I have this code (after adding an object, I redirect user to edit that object):
[PrivilegeRequirement("DB_ADMIN")]
public ActionResult Add()
{
MongoDataContext dc = new MongoDataContext();
var model = new ModelObj();
dc.Models.Insert(model);
return this.RedirectToAction("Edit", new { pId = model.Id, });
}
[PrivilegeRequirement("DB_ADMIN")]
public ActionResult Edit(ObjectId? pId)
{
MongoDataContext dc = new MongoDataContext();
var model = dc.Models.FindOneById(pId);
if (model == null)
{
Session["error"] = "No model with this ID found.";
return this.RedirectToAction("");
}
return this.View(model);
}
However, pId is always null, making the FindOneById always return null. I have debugged and made sure that the Id had value when passing from Add Action. Moreover, I tried adding a testing parameter:
[PrivilegeRequirement("DB_ADMIN")]
public ActionResult Add()
{
MongoDataContext dc = new MongoDataContext();
var model = new ModelObj();
dc.Models.Insert(model);
return this.RedirectToAction("Edit", new { pId = model.Id, test = 10 });
}
[PrivilegeRequirement("DB_ADMIN")]
public ActionResult Edit(ObjectId? pId, int? test)
{
MongoDataContext dc = new MongoDataContext();
var model = dc.Models.FindOneById(pId);
if (model == null)
{
Session["error"] = "No model with this ID found.";
return this.RedirectToAction("");
}
return this.View(model);
}
When I debug, I received the test parameter in Edit Action with value of 10 correctly, but pId is null. Please tell me what I did wrong, and how to solve this problem?
I would suspect that the ObjectId is not serializing/deserializing correctly. Given that it doesn't make for a great WebAPI anyway, I generally use a string and convert within the method to an ObjectId via the Parse method (or use TryParse):
public ActionResult Edit(string id, int? test)
{
// might need some error handling here .... :)
var oId = ObjectId.Parse(id);
}
You can use ToString on the ObjectId to convert it to a string for calling:
var pId = model.Id.ToString();
Related
I am working on an auction application and I am creating a method so that the admins can submit an excel spreadsheet that will create a new auction and store it in the database. So first I made a class (model) Uploadfile like this:
[NotMapped]
public class UploadFile
{
[Required]
public HttpPostedFileBase ExcelFile { get; set; }
}
I used NotMapped because I am trying to understand how to create and use models that aren't stored in my database and this is where my issue and misunderstanding lies.
I created a controller, which I did manually since UploadFile is not an entity with a key as such:
public class FileUploadsController : Controller
{
private AuctionEntities db = new AuctionEntities();
// GET: FileUploads
public ActionResult Index()
{
UploadFile UploadFile = new UploadFile();
return View(UploadFile);
}
[HttpPost]
public ActionResult Index(UploadFile UploadFile)
{
if (ModelState.IsValid)
{
if (UploadFile.ExcelFile.ContentLength > 0)
{
if (UploadFile.ExcelFile.FileName.EndsWith(".xlsx") || UploadFile.ExcelFile.FileName.EndsWith(".xls"))
{
XLWorkbook wb;
// in case if the file is corrupt
try
{
wb = new XLWorkbook(UploadFile.ExcelFile.InputStream);
}
catch (Exception ex)
{
ModelState.AddModelError(String.Empty, $"Check your file. {ex.Message}");
return View();
}
IXLWorksheet ws = null;
try // in case the sheet you are looking for is not found
{
ws = wb.Worksheet("sheet1");
}
catch
{
ModelState.AddModelError(String.Empty, "Sheet not found");
return View();
}
var firstRowUsed = ws.FirstRowUsed();
var auctionRow = firstRowUsed.RowUsed().RowBelow();
// create auction
string auctionName = auctionRow.Cell(1).Value.ToString();
DateTimeOffset startDate = DateTimeOffset.Parse(auctionRow.Cell(2).Value.ToString());
DateTimeOffset endDate = DateTimeOffset.Parse(auctionRow.Cell(3).Value.ToString());
string folderName = auctionRow.Cell(4).Value.ToString();
Models.Auction auction = new Models.Auction(auctionName, startDate, endDate, folderName);
db.Auctions.Add(auction);
// find the next table
var nextRow = auctionRow.RowBelow();
while (nextRow.IsEmpty())
{
nextRow = nextRow.RowBelow();
}
const int catNameCol = 1;
var catRow = nextRow.RowUsed().RowBelow();
// get categories from ws table and add to the auction
while (!catRow.Cell(catNameCol).IsEmpty())
{
string catName = catRow.Cell(1).Value.ToString();
int seqNo = Convert.ToInt32(catRow.Cell(2).Value.ToString());
string fileName = catRow.Cell(3).Value.ToString();
Cat cat = new Cat(auction.AuctionId, catName, seqNo, fileName);
auction.Cats.Add(cat);
catRow = catRow.RowBelow();
}
var findNextRow = catRow.RowBelow();
while (findNextRow.IsEmpty())
{
findNextRow = findNextRow.RowBelow();
}
const int itemNameCol = 1;
var itemRow = findNextRow.RowUsed().RowBelow();
while(!itemRow.Cell(itemNameCol).IsEmpty())
{
string itemName = itemRow.Cell(1).Value.ToString();
string itemDesc = itemRow.Cell(2).Value.ToString();
string catName = itemRow.Cell(3).Value.ToString();
string modelNo = itemRow.Cell(4).Value.ToString();
decimal retailValue = Convert.ToDecimal(itemRow.Cell(5).Value.ToString());
string fileName = itemRow.Cell(6).Value.ToString();
decimal initialBid = Convert.ToDecimal(itemRow.Cell(7).Value.ToString());
decimal increment = Convert.ToDecimal(itemRow.Cell(8).Value.ToString());
Cat itemCat = null;
foreach(var cat in auction.Cats)
{
if(catName == cat.CatName)
{
itemCat = cat;
}
}
Item item = new Item(itemName, itemDesc, modelNo, retailValue, fileName, startDate, endDate, initialBid, increment, null, null, null, itemCat);
itemCat.Items.Add(item);
itemRow = itemRow.RowBelow();
}
}
else
{
ModelState.AddModelError(String.Empty, "Only .xlsx and .xls files are allowed");
return View();
}
}
else
{
ModelState.AddModelError(String.Empty, "Not a valid file");
return View();
}
}
db.SaveChanges();
return View();
}
Next I thought I would try to create a view again so that I can display where the user uploads the file and see if my method works and this is where I have run into my lack of knowledge in asp.net.
So I tried to create a ViewModel as I have seen since the model I created before was a data model, so that I could use this viewmodel to display the upload on my view page. My ViewModel is simple and is:
public class FileUploadViewModel
{
public HttpPostedFileBase ExcelFile { get; set; }
}
Now, I wanted to create a view page for this viewmodel and it is still treating this model has an entity and giving me an error that it does not have a key etc. I need a viewpage that can access a model with the Excel file in it and I can't seem to figure out how to accomplish this. I have read up on viewmodels and I know how crucial they are in MVC, however I just can't seem to grasp on how to use them. Can someone please help me understand how to use one here?
Basically, I want to use this view page with my model or viewmodel:
My educated guess is that you are getting stuck in the "Add View" window.
You are probably selecting a template that requires a model (e.g. Create), selecting your FileUploadViewModel class as the model and also your context.
What this does is it causes the Visual Studio "wizard" to try to map the model internally to your context, which results on the error you see.
Instead, select Empty (without model) as the template which will gray out the Model and Data Context fields. Your view will then be created without errors.
You can then tell the view to expect your model by adding this at the top:
#model FileUploadViewModel
Make sure you fully qualify FileUploadViewModel (e.g. include the namespace in front).
Your methods should now use the model you specified at the top of the view:
public ActionResult Index()
{
FileUploadViewModel UploadFile = new FileUploadViewModel();
return View(UploadFile);
}
[HttpPost]
public ActionResult Index(FileUploadViewModel UploadFile)
{
}
You do not need the [NotMapped] attribute anywhere here.
I am very beginner in this C# world.
There is this model class:
public class Fund
{
[Required]
public int id { get; set; }
[Required]
public string name { get; set; }
[Required]
public string nickname { get; set; }
}
Although the [Required] annotation is over all properties, an instance like:
Fund f = new Fund();
f.name = "test name";
f.nickname = "test ninckname";
always pass in a test like:
if (ModelState.IsValid)
{
// do stuff
}
How am I supposed to set the model such that an instance like that won't pass in in the ModelState.IsValid test?
Other instances like:
Fund f1 = new Fund();
f1.id = 3;
f1.nickname = "test ninckname";
and
Fund f2 = new Fund();
f2.id = 3;
f2.name = "test name";
are also passing on the test.
EDIT:
The ModelState.IsValid is inside a controller, I am testing the controller actually.
EDIT 2:
That is the controller's method signature:
[HttpPatch]
public ActionResult EditFund(Fund fund)
EDIT 3:
That is my whole test method:
[TestMethod]
public void TestEditInvalidFund()
{
// Arrange
FundController controller = new FundController();
controller.ControllerContext = TestModelHelper.AdminControllerContext();
var _fund = new Mock<IFund>();
_fund.SetupGet(f => f.id).Returns(1);
//_fund.SetupGet(f => f.name).Returns("Fund name");
_fund.SetupGet(f => f.nickname).Returns("Fund nickname");
_fund.Setup(f => f.Edit()).Callback(() => {}).Verifiable();
// Act
var result = (JsonResult)controller.EditFund(_fund.Object);
// Assert
SimpleMessage resultMessage = m_serializer.Deserialize<SimpleMessage>(m_serializer.Serialize(result.Data));
Assert.IsNotNull(resultMessage.status, "JSON record does not contain 'status' required property.");
Assert.IsTrue(resultMessage.status.Equals("fail"), "status must be 'fail'");
}
And that is the whole controller's method:
[HttpPatch]
public ActionResult EditFund(IFund _fund)
{
try
{
if (ModelState.IsValid)
{
//_fund.Edit();
}
else
{
string error_messages = "";
foreach (var e in ModelState.Select(x => x.Value.Errors).Where(y => y.Count > 0).ToList())
{
error_messages += e[0].ErrorMessage + "\n";
}
throw new Exception(error_messages);
}
return MessageHelper(#Resources.Global.success, #Resources.Funds.success_editing_fund, "success");
}
catch (Exception err)
{
return ErrorHelper(#Resources.Global.error, #Resources.Funds.error_editing_fund, err.Message);
}
}
You can't unit test ModelState.IsValid. Well, you can, but you need extra code to do it, and it's not exactly ideal.
Here's my code on github: WithModelStateIsInvalid<TController>()
And here's the NuGet package it's in: https://www.nuget.org/packages/TestBase-Mvc/
And here's how I use it:
UnitUnderTest
.WithModelStateIsInValid()
.Action(model)
.ShouldBeViewResult()
.ShouldBeAnInvalidModel();
—————————————————————————————————————————
The reason you can't unit test it, is that Model Validation happens in the ModelBinding step before your controller action is called.
So to simulate a model validation failure, the easiest way is to invalidate controller.ModelState 'manually' in your unit test code, before calling the action. Which is what the extension method does, but actually it's just one-line:
controller.ModelState.AddModelError("FieldName", #"error message")
(A more sophisticated extension method would probably let you specify which Model key is invalid. PRs always welcome).
[Required]
public int id { get; set; }
This will always be valid since an int is not nullable. It has a default value of 0.
Not sure, why it gets valid, but you can try to explicitly revalidate the model by:
TryValidateModel(fund)
This question already has answers here:
How to convert C# nullable int to int
(20 answers)
Closed 8 years ago.
Hi my create method receives an int how can I allow this to be null-able? So I can use this method without the int sometimes.
public ActionResult Create(int id)
{
var model = new Job { IncidentID = id };
ViewBag.ActionCode = new SelectList(db.ActionTypes, "ActionCode", "ActionCode");
return View(model);
}
Obviously I've tried
(int ? id)
but then here it is not happy as it cant convert int? to int here :
var model = new Job { IncidentID = id };
try this
public ActionResult Create(int? id)
{
var model = new Job { IncidentID = id.GetValueOrDefault(0) };
//or var model = new Job { IncidentID = (int.parse(id) };
ViewBag.ActionCode = new SelectList(db.ActionTypes, "ActionCode", "ActionCode");
return View(model);
}
GetValueOrDefault(0) helps to assigns zero if id has no value or null
or
try this
var model = new Job { IncidentID = id.HasValue ? id.Value : 0 };
Just check if id has value and assign IncidentID only if it has:
public ActionResult Create(int? id)
{
var job = new Job();
if (id.HasValue)
job.IncidentID = id.Value;
ViewBag.ActionCode = new SelectList(db.ActionTypes, "ActionCode", "ActionCode");
return View(job);
}
You may use nullable int as your method parameter. Nullable<T> has a HasValue method which check whether a value has been assigned to the nullable variable. If it returns true, use the Value property to get the value of the variable.
public ActionResult Create(int? id)
{
var model=new Job();
if(id.HasValue)
{
model.IncidentID=id.Value;
}
//to do :return something
}
In my viewmodel, I have a list of items I fetch from the database and then send to the view. I would like to know if it's possible to avoid having to refill the options property whenever I hit a Post action and need to return the model (for validation errors and what not)?
In webforms, this wouldn't be necessary.
Edit: I was not clear. My problem is with the SelectList options I use for my DropDownLists. Everything gets posted, but if I have to return to the view (model is invalid), I have to reload the options from the database! I want to know if this can be avoided.
My viewmodel:
public class TestModel
{
public TestModel()
{
Departments = new List<SelectListItem>();
}
public string Name { get; set; }
public int Department { get; set; }
public IEnumerable<SelectListItem> Departments { get; set; }
}
My view:
#model MvcApplication1.Models.TestModel
#using (Html.BeginForm())
{
#Html.TextBoxFor(m => m.Name)
#Html.DropDownListFor(m => m.Department, Model.Departments)
<input type=submit value=Submit />
}
My controller (do notice the comment on HttpPost):
public ActionResult Index()
{
TestModel model = new TestModel
{
Name = "Rafael",
Department = 1,
Departments = new List<SelectListItem>
{
new SelectListItem { Text = "Sales", Value = "1" },
new SelectListItem { Text = "Marketing", Value = "2", Selected = true },
new SelectListItem { Text = "Development", Value = "3" }
}
};
// Departments gets filled from a database.
return View(model);
}
[HttpPost]
public ActionResult Index(TestModel model)
{
if (!ModelState.IsValid)
{
//Do I have to fill model.Departments again!?!?!?
return View(model);
}
else { ... }
}
Thanks in advance.
Edit: FYI, my solution was to use the Session variable.
Just need to strongly type your view, and change your controller method to have a parameter of that class type.
That is, the view
#model MyNamesspace.Models.MyModel
...
#using (Html.BeginForm())
{
....
}
And you controller method which is posted to.
[HttpPost]
public ActionResult MyAction(MyModel model)
{
...
}
EDIT: Also make sure you have form fields for each property of the model which you need posted to the controller. My example is using Razor too BTW.
I encountered a similar problem when trying to create an Order wizard in MVC (one where each page of the wizard is implemented as a partial view loaded by AJAX). I highly doubt it is the suggested method but my way of solving this was to call a custom MergeChanges method in each action called by my wizard:
public Order MergeChanges(Order newOrder)
{
var sessionHistory = (List<string>)Session["sessionHistory"];
if (sessionHistory == null || sessionHistory.Count == 0)
return MergeChanges(newOrder, -1);
return MergeChanges(newOrder, MasterViewController.GetStepNumberByName(sessionHistory.Last()));
}
public Order MergeChanges(Order newOrder, int step)
{
PreMerge(newOrder);
Order result = null;
try
{
ApplyLookups(ref newOrder);
Order oldOrder = (Order)Session["order"];
if (oldOrder == null)
{
Session["order"] = newOrder;
result = newOrder;
}
else
{
List<TypeHelper.DecoratedProperty<ModelPageAttribute>> props = null;
newOrder.GetType().GetDecoratedProperty<ModelPageAttribute>(ref props);
props = props.Where(p => (p.Attributes.Count() > 0 && p.Attributes.First().PageNumber.Contains(step))).ToList();
foreach (var propPair in props)
{
object oldObj = oldOrder;
object newObj = newOrder;
if (!string.IsNullOrEmpty(propPair.PropertyPath))
{
bool badProp = false;
foreach (string propStr in propPair.PropertyPath.Split('\\'))
{
var prop = oldObj.GetType().GetProperty(propStr);
if (prop == null)
{
badProp = true;
break;
}
oldObj = prop.GetValue(oldObj, BindingFlags.GetProperty, null, null, null);
newObj = prop.GetValue(newObj, BindingFlags.GetProperty, null, null, null);
}
if (badProp)
continue;
}
if (newObj == null)
continue;
var srcVal = propPair.Property.GetValue(newObj, BindingFlags.GetProperty, null, null, null);
var dstVal = propPair.Property.GetValue(oldObj, BindingFlags.GetProperty, null, null, null);
var mergeHelperAttr = propPair.Property.GetAttribute<MergeHelperAttribute>();
if (mergeHelperAttr == null)
{
if (newObj != null)
propPair.Property.SetValue(oldObj, srcVal, BindingFlags.SetProperty, null, null, null);
}
else
{
var mergeHelper = (IMergeHelper)Activator.CreateInstance(mergeHelperAttr.HelperType);
if (mergeHelper == null)
continue;
mergeHelper.Merge(context, HttpContext.Request, newObj, propPair.Property, srcVal, oldObj, propPair.Property, dstVal);
}
}
result = oldOrder;
}
}
finally
{
PostMerge(result);
}
return result;
}
Since my case was doing this with a wizard, only specific values applied to each page so in order to only account for properties known to the current page of the wizard, I've implemented some attributes, a (admittedly over complex) ViewController layer, and a custom validation layer. I can share some more code but the code above does the grunt work if you aren't in such a complex situation. If there is a better way, I hope to learn it from the answers to this question because this was a PITA.
I am surprised this question doesn't come up more often, and I am also surprised the obvious (IMHO) answer isn't standard practice these days: nearly all POSTs should be Ajax-based. This solves a whole slew of problems including
No need to repopulate form data when you have e.g. a validation error, or application error (exception). This is particularly desirable when you have client-side state (in true rich web application fashion).
No compulsion to perform client-side validation. Validation can be 100% server-side (where it must be anyways) and the user experience is nearly the same.
Of course, there is some initial work you need to do to build out a framework for this, for example, I have a set of AjaxUpdate, AjaxNothing, AjaxRedirect, AjaxErrors ... ActionResult types which render Json which is processed by some custom Javascript. But once you get that in place, it's smooth sailing.
I'm getting my feet wet with the Entity Framework and am wondering if there is a way for me to check model state without model binding happening.
Say I create a user primarily from code, is there a way for me to check to make sure it is valid according to my predefined data annotations before I update?
public ActionResult Index()
{
User u = new User();
u.Username = "test";
u.Password = "test";
u.Email = "test";
DefaultContext db = new DefaultContext();
if (ModelState.IsValid)
{
db.Users.Add(u);
db.SaveChanges();
Response.Write(u.Id);
}
else
// model is not valid
return View();
}
The above code does not work because there is no binding happening. Or maybe I am confused of the process.
Thanks.
ModelState won't be available because it's set up by the model binder. The ModelBinder didn't bind a model so it didn't run validation thus ModelState will be empty.
But that doesn't stop you from using attributes and performing validation.
Assuming you are using the DataAnnotation attributes, you can run the validation without model binding.
Given this class:
public class Widget
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
public decimal Price { get; set; }
}
You can do this:
var widget = new Widget
{
Id = 12,
Price = 15.57M
};
var context = new ValidationContext(widget, null, null);
var results = new List<ValidationResult>();
if( Validator.TryValidateObject( widget, context, results, true ) )
{
//Validation Successful
}
else
{
//Validation Failed
}
The Name property was not set and the TryValidateObject() will fail. The results collection will have the actual error that occurred.
you can use TryValidateModel or ValidateModel
User u = new User();
u.Username = "test";
u.Password = "test";
u.Email = "test";
if (TryValidateModel(u))
{
}else{
}