Persisting unbound viewmodel data when posting - c#

I am currently in the process of getting accustomed to MVC, having come from ASP.Net.
So far I have found ways to achieve what I want to do, but with this one I am getting a "This cannot be the easiest way" moment.
Scenario:
I am migrating a quoting application to MVC that has an existing database so my model classes are auto-generated. I have created a viewmodel class for each controller action that needs to display data to the user.
The edit quote viewmodel looks like this:
public class QuoteEdit_ViewModel
{
public SelectList DelLocations { get; set; }
public int QuoteID { get; set; }
public string QuoteNo { get; set; }
public string EnquiryNo { get; set; }
public string SalesPerson { get; set; }
public string Exceptions { get; set; }
public string CreatedBy { get; set; }
public string ModifiedBy { get; set; }
[Required]
[Display(Name = "Equipment Overview")]
public string EquipmentOverview { get; set; }
public DateTime Created { get; set; }
public DateTime Modified { get; set; }
[Required]
public int? Validity { get; set; }
[Required]
[Display(Name = "Minimum Delivery Weeks")]
public int? DeliveryMin { get; set; }
[Required]
[Display(Name = "Maximum Delivery Weeks")]
public int? DeliveryMax { get; set; }
[Required]
[Display(Name = "Delivery Location")]
public int? DelLocationID { get; set; }
public List<Constants.QPT> PackTypes { get; set; }
public List<Constants.QE> Equipments { get; set; }
public List<Constants.QEEx> Extras { get; set; }
}
The lists at the bottom contain equipment lines etc that are junction tables in the database.
Currently I can edit this and post the data back to the database and it works perfectly.
The part that seems messy is the following, specifically the part after the if:
public ActionResult Save(QuoteEdit_ViewModel VM)
{
Quote a = DAL.DB.Quotes.Where(x => x.QuoteID == VM.QuoteID).Single();
TryUpdateModel(a);
if (ModelState.IsValid)
{
DAL.DB.SaveChanges();
return RedirectToAction("Dashboard", "Home");
}
VM.DelLocations = DAL.GetDeliveryLocationDropdown();
var QData = DAL.GetQuoteEditVM(VM.QuoteID);
VM.QuoteNo = QData.QuoteNo;
VM.EnquiryNo = QData.EnquiryNo;
VM.SalesPerson = QData.SalesPerson;
VM.PackTypes = QData.PackTypes;
VM.Equipments = QData.Equipments;
VM.Extras = QData.Extras;
VM.Created = QData.Created;
VM.CreatedBy = QData.CreatedBy;
VM.Modified = QData.Modified;
VM.ModifiedBy = QData.ModifiedBy;
return View("Edit", VM);
}
Currently I need to reload the entire viewmodel and repopulate any fields that were not bound in the view as their values are lost during the POST.
I have read in other posts that you can use hiddenfor, but can this be used for Lists as well?
Also is this the correct way to approach this or am I completely missing the point of MVC?

Do not use HiddenFor. You're going about the correct way. The only change I would make is factoring out your common code into another method(s) that both the GET and POST actions can utilize.
private void PopulateQuoteEditViewModel(QuoteEdit_ViewModel model)
{
mode.DelLocations = DAL.GetDeliveryLocationDropdown();
var QData = DAL.GetQuoteEditVM(model.QuoteID);
model.QuoteNo = QData.QuoteNo;
model.EnquiryNo = QData.EnquiryNo;
model.SalesPerson = QData.SalesPerson;
model.PackTypes = QData.PackTypes;
model.Equipments = QData.Equipments;
model.Extras = QData.Extras;
model.Created = QData.Created;
model.CreatedBy = QData.CreatedBy;
model.Modified = QData.Modified;
model.ModifiedBy = QData.ModifiedBy;
}
Then:
public ActionResult QuoteEdit()
{
var model = new QuoteEdit_ViewModel();
PopulateQuoteEditViewModel(model);
return View(model);
}
[HttpPost]
public ActionResult QuoteEdit(QuoteEdit_ViewModel model)
{
if (ModelState.IsValid)
{
...
}
PopulateQuoteEditViewModel(model);
return View(model);
}
Other Comments
Don't use TryUpdateModel. It's not meant to be used the way you are here. The correct approach is to map over the posted values from your view model. You can either do this manually, or utilize a library like AutoMapper. Either way, you don't want to just willy-nilly overwrite anything on your database entity based on raw posted data as you're doing.
You should never post the ID of the entity you're editing, but rather rely on obtaining it from the URL via a route param. If the URL is changed to a different ID, you are literally editing a different thing, and you can add object-level permissions and such to control who can edit what. However, the ID that's posted can be manipulated, and if you aren't careful (as you aren't being here), then a user can tamper with the ID to mess with objects they potentially shouldn't be editing.

Related

How should I pass the author of the change into PUT/POST methods?

I have a Category class which looks like this:
public class Category
{
[Required]
public Guid Id { get; set; }
[Required]
public string CategoryName { get; set; }
public string CategoryDescription { get; set; }
public DateTime CreatedDate { get; set; }
public bool isDeleted { get; set; } = false;
public virtual ICollection<UpdateInfo> Updates { get; set; } = new List<UpdateInfo>();
}
And a UpdateInfo class which looks like this (with enum):
public enum Status
{
Created,
Changed,
Deleted
}
public class UpdateInfo
{
public Guid Id { get; set; }
public string Author { get; set; }
public DateTime Date { get; set; }
public Status Status { get; set; }
public virtual Category Category { get; set; }
}
I am looking for a proper way to pass the author into PUT/POST/DELETE methods together with Id or CategoryDTO passed from body. I tried it with POST method first, and I need an opinion, and the proper way to do this.
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<Category>> PostCategory(CategoryDTO categoryDTO, string author)
{
_logger.LogInformation("Creating a new category DB object from a DTO objcect passed in PostOperation method.");
var category = _mapper.Map<CategoryDTO, Category>(categoryDTO);
if (CategoryRepeat(category.CategoryName))
{
return Conflict("Such category already exists.");
}
_logger.LogInformation("Adding an initial update status.");
var initialUpdate = new UpdateInfo { Author = author, Status = Status.Created, Date = DateTime.UtcNow, Id = Guid.NewGuid(), Category = category };
category.Updates.Add(initialUpdate);
try
{
_logger.LogInformation($"Trying to save created category {category.Id} into the Database.");
_context.Categories.Add(category);
await _context.SaveChangesAsync();
_logger.LogInformation($"Created category id: {category.Id} saved into the Database successfully.");
}
catch (DbUpdateConcurrencyException ex)
{
_logger.LogError(ex.Message);
return BadRequest(new { message = ex.Message });
}
return CreatedAtAction("GetCategory", new { id = category.Id }, category);
}
The way you did it by passing it as a separate variable might be more flexible if you need to pass the author in some requests but not others. So you did good. If you see yourself that you are passing the author everywhere, it would be better to change the CategoryDTO model instead.
Another way would be to make another DTO for the specific action you are looking for. For example CreateCategoryDTO for "create" POST requests and change the model there instead. It really depends on your coding style and the whole "coding standard" of your project.
If I understood your problem correctly, you could also pass the author in the request header. you can create a custom header and set it when sending the request then you can get the info like string author = Request.Headers["Header-Author"];
another way is to add the property to the existing CategoryDTO itself. then get it like string author = categoryDTO.Author;
one way is as you have done by passing as a separate parameter.

MVC datetime list not saving

I have a simple MVC4 model that adds a DateTime.Now to a List<DateTime>() list.
However when I do an EntityState.Modified, the changes are not being kept.
I've debugged this by modifying another property in the model and that saves fine.
So I really am at a loss as to why this is not saving. If anyone has any ideas as to why its not saving that would be life saver material:
The Model:
public class Page
{
public int Id { get; set; }
public string PageURL { get; set; }
public string Name { get; set; }
public string Title { get; set; }
public List<DateTime> Visits { get; set; }
public Page()
{
Visits = new List<DateTime>();
}
}
Here's my code:
private ApplicationDbContext db = new ApplicationDbContext();
public ActionResult CookiePolicy()
{
var page = db.Pages.FirstOrDefault(c => c.PageURL == "cookiepolicy");
page.Visits.Add(DateTime.Now); // this list of datetime objects does not get updated
page.Title = "test "; //but this property does
ViewBag.Title = page.Title;
db.Entry(page).State = EntityState.Modified;
db.SaveChanges();
return View(page);
}
Edit Fabio Luz mentioned:
"collection of primitive types (like int, DateTime, bool) are not
supported"
So the solution below seems like the right option.
Ok, so after some deliberation. I decided to create a new model called vist and have this as the list instead of datetime:
public class Visit
{
public int Id { get; set; }
public DateTime DateTime { get; set; }
public BrowserType BrowserType { get; set; }
public String Duration { get; set; }
public int PageId { get; set; }
public virtual Page Page { get; set; }
public Visit()
{
DateTime = DateTime.Now;
BrowserType = BrowserType.Other;
}
}
There are benefits to this. Now I can store more information then just the datetime.
So for anyone who had the same problem as me. Consider pushing it out into its own model for greater flexibility.
Like Fabio Luz mentioned in his comment primitive type collections aren't supported. A collection within an class retrieved from a context is generally assumed to represent a One-to-Many / Many-to-Many relationship.
When building models keep in mind how they would be represented in a SQL table, and having a column that has a collection within is not supported in such a structure. Now, if you were referencing another object (table) than the object (table record) would have certain properties, such as a primary key etc.
Hope this helps.
Edit:
Here is a sample model you might want to consider:
public class Page
{
public int Id { get; set; }
public string PageURL { get; set; }
public string Name { get; set; }
public string Title { get; set; }
public virtual IQueriable<Visit> Visits { get; set; }
}
public class Visit
{
// ... properties related to data you wish to retain about the visit
public virtual Page Page { get; set; } // navigation property
}

ASP.NET MVC and Mongodb references

I have problem with my application. In my simple blog application I have Post and Category collection:
Post.cs
public class Post
{
[ScaffoldColumn(false)]
[BsonId]
public ObjectId PostId { get; set; }
[ScaffoldColumn(false)]
[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
public DateTime Date { get; set; }
[Required]
public string Title { get; set; }
[ScaffoldColumn(false)]
public string Url { get; set; }
public ObjectId CategoryId { get; set; }
[UIHint("WYSIWYG")]
[AllowHtml]
public string Details { get; set; }
[ScaffoldColumn(false)]
public string Author { get; set; }
[ScaffoldColumn(false)]
public int TotalComments { get; set; }
[ScaffoldColumn(false)]
public IList<Comment> Comments { get; set; }
}
and category.cs
public class Category
{
[ScaffoldColumn(false)]
[BsonId]
public ObjectId CategoryId { get; set; }
[ScaffoldColumn(true)]
[Required]
public string Name { get; set; }
}
PostControler:
[HttpPost]
public ActionResult Create(Post post)
{
if (ModelState.IsValid)
{
post.Url = post.Title.GenerateSlug();
post.Author = User.Identity.Name;
post.Date = DateTime.Now;
post.CategoryId =
_postService.Create(post);
return RedirectToAction("Index");
}
return View();
}
When I create new post, I want to choose category from list and save "_id" of category in post collection. I don't have any idea to resolve this. Can anybody give some suggestions, to solve this.
I don't really understand the problem here, but let's walk through this step-by-step:
First, you'll have to populate the list of available categories, so you'll have to call FindAll on your Category collection.
Second, you will need to pass this list to the frontend, so you need a view model class that contains some kind of dictionary so you can bind this to a dropdown (<select>), for instance, i.e.
class PostCreateViewModel
{
Dictionary<string, string> Categories {get; set;}
/// ...
}
[HttpGet]
public ActionResult Create()
{
var categories = _categoryService.All();
var vm = new PostCreateViewModel();
vm.Categories = categories.ToDictionary(p => p.CategoryId.ToString());
// etc.
return View(vm);
}
Third, your Post handler should verify that the CategoryId is correct:
[HttpGet]
public ActionResult Create(Post post)
{
// Make sure the given category exists. It would be better to have a
// PostService with a Create action that checks this internally, e.g.
// when you add an API or a different controller creates new posts as
// a side effect, you don't want to copy that logic.
var category = _categoryService.SingleById(post.CategoryId);
if(category == null)
throw new Exception("Invalid Category!");
// Insert your post in the DB
// Observe the PRG pattern: Successful POSTs should always redirect:
return Redirect("foo");
}
Some details can be tricky, for instance I'm assuming that post.CategoryId will be populated through a model binder, but you might have to write a custom model binder for ObjectId or use a ViewModel class with a string and parse the string using ObjectId.Parse. In general, I'd suggest using ViewModels everywhere and use the help of something like AutoMapper to avoid writing tons of boilerplate copy code.

ASP.Net MVC - Passing data between Views

I'm currently trying to create an XML based website which has access to a feed URL. The XML feed is queried by adding url parameters to the current URL, and so I am using a form with GET method to add parameters to the URL.
I currently have a property search form which will search for properties within the feed by adding xml parameters to the url like so:
/Sales/?minprice=300000&maxprice=500000
This works perfectly and the correct results are shown to the user. However, if I was to use a filter form which filtered these properties by highest price for example, the feed parameters would be removed when the filter form is submitted. The new URL after the filter would be for example:
/Sales/?priceSort=descending
As you can see, the minprice and maxprice fields have been removed completely, leaving me with unwanted properties.
Currently, to combat this I am using sessions to store the URLs for each page and then combining them to make 1 url. I understand that using sessions within MVC based applications isn't exactly recommended.
So, I'm really just wondering if there is a better way to store the url's rather than using sessions?
Any help would be much appreciated.
Thanks in advance.
SOME CODE SNIPPETS OF THE SITE:
Model and ViewModel
public class ResultsViewModel
{
public PropertyResult[] Property { get; set; }
}
public class PropertyResult
{
public int Count { get; set; }
public int Pid { get; set; }
public int RentalPeriod { get; set; }
public string Price { get; set; }
public string Address { get; set; }
public string NameNumber { get; set; }
public string SA1 { get; set; }
public string SA2 { get; set; }
public string Town { get; set; }
public string City { get; set; }
public string County { get; set; }
public string Postcode { get; set; }
public string LocationCode { get; set; }
public string PriceText { get; set; }
public string Primary1 { get; set; }
public string Secondary1 { get; set; }
public string Secondary2 { get; set; }
public string Description { get; set; }
public string Period { get; set; }
public int Bedrooms { get; set; }
public int Receptions { get; set; }
public int Bathrooms { get; set; }
public int Garages { get; set; }
public int Gardens { get; set; }
public bool Featured { get; set; }
public int Views { get; set; }
}
Controller
try
{
var xml = XElement.Load(resultsFeed);
var query = (from props in xml.Descendants("property")
select new PropertyResult
{
// property id
Pid = Convert.ToInt32(props.Attribute("id").Value),
// Rooms Count
Bedrooms = Convert.ToInt32(props.Attribute("bedrooms").Value),
Receptions = Convert.ToInt32(props.Attribute("receptions").Value),
Bathrooms = Convert.ToInt32(props.Attribute("bathrooms").Value),
Gardens = Convert.ToInt32(props.Attribute("gardens").Value),
Garages = Convert.ToInt32(props.Attribute("garages").Value),
// 1 = sales prop, 4 = lettings prop
RentalPeriod = Convert.ToInt32(props.Attribute("rentalperiod").Value),
Period = props.Attribute("period").Value,
// address
Address = props.Element("useAddress").Value,
NameNumber = props.Element("num").Value,
SA1 = props.Element("sa1").Value,
SA2 = props.Element("sa2").Value,
Town = props.Element("town").Value,
City = props.Element("city").Value,
County = props.Element("county").Value,
Postcode = props.Element("postcode").Value,
// location code
LocationCode = props.Element("locationcodes").Value,
Featured = Convert.ToBoolean(props.Attribute("featured").Value),
// description
Description = props.Element("summaryDescription").Value,
// price
Price = props.Attribute("price").Value,
PriceText = props.Element("pricetext").Value,
// images
Primary1 = "http://lb.dezrez.com/Imaging/PictureResizer.ASP?Position=1&AgentId=" + eaid + "&BranchId="+ bid + "&width=1000&Category=Primary&PropertyId=",
Secondary1 = "http://www.dezrez.com/estate-agent-software/ImageResizeHandler.do?&photoID=2&AgentID=1239&BranchID=1976&Width=1000&PropertyId=",
Secondary2 = "http://www.dezrez.com/estate-agent-software/ImageResizeHandler.do?&photoID=3&AgentID=1239&BranchID=1976&Width=1000&PropertyId=",
}).ToArray();
View
I'm currently accessing each node like so:
#Model.Property[i].Gardens
In MVC you need to pass all the needed parameters (or rely in some store as Session, Cache, Db).
So, when sorting, you are just sending the column and order... in this case, you need to post also the filter values.
The correct way to do this, is having a ViewModel with all the filters and sorting parameters... and when you return from filtering or sorting, you can render the current filters.
So, besides filling the filter inputs with the current filters, you should craft the links to take into account all the parameters. For instance: when ordering, you pass also current filters... or if you change the filters you should maintain sortorder passing it on post.
Some code:
Your ViewModel:
public class SalesFilter{
public int? MinPrice {get; set;}
public int? MaxPrice {get; set;}
public int? IdTypeOfSale {get; set;}
...
...
...
public IEnumerable<Sale> FilteredValues {get; set;}
//SelectLists if you need that your filters being DropDownLists
public SelectList TypesOfSales {get; set;}
}
Your Controller:
public ActionResult Sales(){
var model = new SalesFilter();
model.FilteredValues = db.YourSales.Where(/*your init conditions*/);
//set other initial values in your ViewModel
return View(model);
}
[HttpPost]
public ActionResult Sales(SalesFilter filters){
model.FilteredValues = db.YourSales.Where(/*use the conditions of your filter */);
model.TypesOfSales = new SelectList(db.TypesOfSales, "idType", "Name", filter.IdTypeOfSale);
return View(model);
}
Consider using a Domain Model (all your business data, etc.) and separate View Models and extension methods that will transform your domain model to a specific view model and vice versa. Decoupling your business model from your view model using the transform indirection gives you the opportunity to use simple, easy to use view models that fit your view.

TryUpdateModel sets property to null on insert even though a value is specified, using MVC4 and Entity Framework

I hope this is an easy one.
Using MVC4 with Entity Framework 4.1.
I'am trying to update a model with some properties passed through from a View with a custom view model's properties.
I have this class.
public class SitePage
{
public System.Guid PageID { get; set; }
public int PageTypeID { get; set; }
public string PageTitle { get; set; }
public Nullable<int> RegionId { get; set; }
public Nullable<int> LockLevelId { get; set; }
public System.DateTime UpdatedDate { get; set; }
public System.Guid UpdatedById { get; set; }
public System.Guid CreatedById { get; set; }
public System.DateTime CreatedDate { get; set; }
public string Url { get; set; }
public int PageStateId { get; set; }
public string SummaryImage { get; set; }
}
My controller
public ActionResult Update(AddPageViewModel model)
{
if (ModelState.IsValid)
{
var g = new Guid(model.PageId);
SitePage UpdatePage = dbSite.SitePages.FirstOrDefault(m => m.PageID == g);
//This is a page update. Not updating all properties
UpdatePage.PageTitle = model.PageTitle;
UpdatePage.GetFirstSection.PageSectionText.PageText = model.PageText;
UpdatePage.PageTypeID = (int)SitePageType.PageTypes.CommunityPage;
UpdatePage.RegionId = model.Region
UpdatePage.CreatedById = userId;
UpdatePage.CreatedDate = DateTime.UtcNow;
UpdatePage.UpdatedById = userId;
UpdatePage.UpdatedDate = DateTime.UtcNow;
TryUpdateModel(UpdatePage);
dbSite.Save();
return RedirectToAction("Community_test", "Community", new { id = UpdatePage.Url });
}
else
{
return RedirectToAction("Community_test", "Community");
}
}
Passing this page view model to the controller
public class AddPageViewModel
{
public string PageTitle { get; set; }
public string PageText { get; set; }
public int Region { get; set; }
public string TagString { get; set; }
public string ParentPageId { get; set; }
public string PageId { get; set; }
}
If i debug I can see that the SitePage instance UpdatePage has all the correct values when TryUpdateModel is called. However all values except RegionId are updated correctly in the db. RegionId gets set as null. If i run a trace on the server the sql entity framework generates is indeed setting RegionId to null even though my model has a value. This problem is intermittent! Some times it works, other it doesnt. 1 in 4 will update correctly and i cant quite see where im going wrong. Im newish to this stuff really.
Our classes are generated from the database and we are using an edmx file. Im not sure if i can make any changes to these classes without them just being overridden the next time we update model from database.
Any ideas or more info needed, give me a shout. Cheers
TryUpdateModel takes the values from the form and populate the given model. This means that setting any values on UpdatePage isn't necessary.
if (ModelState.IsValid)
{
var updatePage = dbSite.SitePages.FirstOrDefault(m => m.PageID == model.PageId);
TryUpdateModel(updatePage);
dbSite.Save();
//...
}
It is good practice to also specify the properties that should be updated.
TryUpdateModel(updatePage, new string[] { "PageTitle", "RegionId", etc.. });
Regarding RegionId getting set to Null.
If i debug I can see that the SitePage instance UpdatePage has all the correct values when TryUpdateModel is called.
Before TryUpdateModel is called or right after?

Categories