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.
Related
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.
I've two tables as Models in my project Like:-
public partial class TblAlbum
{
public int ID { get; set; }
public string Album { get; set; }
public string Artists { get; set; }
public Nullable<int> AudioID { get; set; }
public Nullable<int> VideoID { get; set; }
public virtual TblVideo TblVideo { get; set; }
public virtual TblAudio TblAudio { get; set; }
}
and
public partial class TblAudio
{
public int ID { get; set; }
public string Name { get; set; }
public string Alt { get; set; }
public string Artist { get; set; }
public string Image { get; set; }
public int LangID { get; set; }
public virtual TblLanguage TblLanguage { get; set; }
public virtual ICollection<TblAlbum> TblAlbums { get; set; }
}
Now I've Made a ViewModel as GetDetailsVM that have access to both tables and has the LINQ Query as:-
public class GetDetailsVM
{
private MusicEntities db = new MusicEntities();
public IEnumerable<dynamic> GetAudio()
{
var AudioList = from au in db.TblAudios
join al in db.TblAlbums on au.ID equals al.AudioID into ar
from al in ar.DefaultIfEmpty()
select new { au,al };
return AudioList.ToList();
}
}
My ViewModel(AudioAlbumVM) to read Getaudio() should be something like this:-
public class AudioAlbumVM
{
public IEnumerable<dynamic> AudioObjList { get; set; }
public string AlbumName { get; set; }
}
Now I want to access this ViewModel in my controller and then use it in my cshtml.
My Controller:-
public ActionResult Audio()
{
ViewBag.Title = "Audio";
var AudioSummary = new GetDetailsVM();
var viewModel = new AudioAlbumVM
{
AudioObjList = AudioSummary.GetAudio().First()
};
return View(viewModel);
}
UPDATE
My View(Audio.cshtml) is as follows:-
#model GarhwalMusic.Models.AudioAlbumVM.AudioObjList
<a class="art" href="single.html"> #Model.AudioObjList</a>
I was going through this question LINQ left join only works in the ActionResult but I'm completely lost . Need help and explanation on how to create AudioAlbumVM using another ViewModel(GetDetailsVM) then in controller and then in cshtml. Any help is greatly appreciated!!
Taking this from your comments
Now I'm getting this error:- The model item passed into the dictionary is of type 'GarhwalMusic.Models.AudioAlbumVM', but this dictionary requires a model item of type 'System.Collections.Generic.IEnumerable`1[GarhwalMusic.Models.AudioAlbumVM]'.
Problem is with this code
var viewModel = new AudioAlbumVM //its a single model
{
AudioObjList = AudioSummary.GetAudio().First()
};
But it seems like your view is expecting a model of type IEnumerable<GarhwalMusic.Models.AudioAlbumVM> or (List<GarhwalMusic.Models.AudioAlbumVM>). You need to either pass a List<GarhwalMusic.Models.AudioAlbumVM> to your view or change your view to accept a model of type GarhwalMusic.Models.AudioAlbumVM.
But looking at your code I assume that you are trying to pass the collection of AudioObjList into your View. So your view must accept a model of type
GarhwalMusic.Models.AudioAlbumVM.AudioObjList and you need to just Pass in only the viewModel.AudioObjList from your controller.
EDIT 1: After you added more details of your view, You are making a mistake in the way you are accessing the data.
your Model.AudioObjList is of type IEnumerable<dynamic> and how can you just print something on the Vie like #Model.AudioObjList ?? I am referring to this code
<a class="art" href="single.html"> #Model.AudioObjList</a>
You need to typecast this dynamic type into what ever object you have created in your linq and then extract its property value. Something like
#((yourObject)Model.AudioObjList[0]).propertyName
(yourObject)Model.AudioObjList[0] type casts your dynamic data into a type yourObject.
you need [0] because your AudioObjList is of type IEnumerable<dynamic>. So taking the first data.
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.
I have a model Team which looks like this:
public class Team
{
[Key]
public int TeamId { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public virtual Group Group { get; set; }
}
I have a method to Edit it:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(Team team)
{
using (var db = new DbConnection())
{
if (ModelState.IsValid)
{
db.Entry(team).State = EntityState.Modified;
db.Groups.Attach(team.Group);
db.SaveChanges();
}
return PartialView(team);
}
}
However it is not saving the change to the Group column. Meanwhile Create method does work although I see no difference between them:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Team team)
{
using (var db = new DbConnection())
{
if (ModelState.IsValid)
{
db.Groups.Attach(team.Group);
db.Teams.Add(team);
db.SaveChanges();
return RedirectToAction("Index");
}
return PartialView(team);
}
}
The values for the Group come the same in both methods. I bring the values to the form like this (again, same thing for both):
ViewBag.Groups = db.Groups.ToList().Select(g => new SelectListItem()
{
Text = g.Code,
Value = g.GroupId.ToString(),
Selected = false
}).ToList();
and use them like this:
#Html.DropDownListFor(model => model.Group.GroupId, (List<SelectListItem>)ViewBag.Groups)
Can someone please explain what the difference between the two methods is (in terms of one saving Group and other - not)?
P.S. Using both the latest MVC and EF.
Apparently it is a must (?) (at least I couldn't find anything better) to add a field matching the type of the Group in order to be able to easily save. No need to attach anything this way. Now my model looks like:
public class Team
{
[Key]
public int TeamId { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public int? GroupId { get; set; }
public virtual Group Group { get; set; }
}
and my Save and Create methods got this line:
db.Groups.Attach(team.Group);
removed.
Maybe a simple question, but I can't seem to figure it out. Saving a collection to a model when adding a model to the database isn't working. I have a site which uses asp.net MVC and entity framework.
The models:
public class Event
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public ICollection<EventRange> Ranges { get; set; }
}
public class EventRange
{
public int Id { get; set; }
public string RangeName { get; set; }
public string RangeDescription { get; set; }
public int Capacitiy { get; set; }
}
The controller actions:
[HttpPost]
public ActionResult Create(Event model)
{
ICollection<EventRange> eventRanges = new Collection<EventRange>();
var range = new EventRange {RangeName = "testrange", RangeDescription = "test", Capacitiy = 5}
eventRanges.Add(range);
model.Ranges = eventRanges;
db.Events.Add(model);
db.SaveChanges();
return View();
}
public ActionResult Events()
{
return View(db.Events);
}
When setting a breakpoint in the Events action and evaluated the query, the Range isn't saved to the event:
Code Screenshot
Note that that the database created for the eventrange model by EF does save the range:
EF DB Screenshot
Am I doing something wrong?
What if you mark the Ranges property as virtual?
public virtual ICollection<EventRange> Ranges { get; set; }