What I want to do
I am very new to MVC.
I'm trying to create a page that allows users to perform the following actions on the same page:
View the list (table)
Add a new item (Filling the form and clicking the Add button should update the table)
Delete an item from the list (Clicking the Delete button in a row should update the table)
A simple example looks like this but I actually have two lists on one page (Fees and Costs):
Question
What would be the best way to achieve this?
Should I go with Dylan Beattie's method posted here which would look something like this?
public ActionResult MyAction(string submitButton, MyViewModel form)
{
switch (submitButton)
{
case "AddFee":
return (AddFee(form));
case "AddCost":
return (AddCost(form));
case "RemoveFee":
return (RemoveFee(form));
case "RemoveCost":
return (RemoveCost(form));
}
}
public ActionResult AddFee(MyViewModel form)
{
Fee newFee = ....; // Get entered data from `form`
_repository.InsertFee(newFee);
return View("Create"); //Back to the original page
}
Or is there any other recommended methods to handle this such as using JavaScript?
You could create the table as a partial view and re render this via ajax.
Wrap the partial view in a div and Wrap the form in #using (Ajax.BeginForm(.... and target the wrapper div. Your controller action that is targeted by the ajax request will need to return a partial view.
Here is a simple example
public class HomeController : Controller
{
public ActionResult Index()
{
MYvm vm = new MYvm() { id = 1, name = "This is my View Model" };
return View(vm);
}
public ActionResult DA(MYvm vm)
{
vm.name = "CHANGED";
return PartialView("Part", vm);
}
View:
#model MvcApplication1.Controllers.HomeController.MYvm
#{
ViewBag.Title = "Home Page";
}
#using (Ajax.BeginForm("DA", "Home", new AjaxOptions() { UpdateTargetId = "cont", HttpMethod = "Get" }))
{
<div>
Id: #Html.EditorFor(model => model.id)
</div>
<div>
Name: #Html.EditorFor(model => model.name)
</div>
<input type="submit" value="SubmitForm" />
}
<div id="cont">
#{Html.RenderPartial("part", Model);}
</div>
Partial View
#model MvcApplication1.Controllers.HomeController.MYvm
#{
ViewBag.Title = "part";
}
<h2>part</h2>
#Model.name
Should I go with [previous SO answer]
No. That answer was for a different scenario where the question had a form with two submit buttons that wanted to do two different actions (and wasn't even the accepted answer to that question).
Your sample screenshot indicates that some javascript/jquery and ajax would solve the issue cleanly.
As you're new to MVC, try to keep it relatively simple. Break up the page into separate parts:
the containing page
the edit form
the list with remove
the edit/list work independently and should be written in a way that they could be put on any other page - the page is just there to contain them and doesn't do much else (obviously your real page will contain more, but add those parts as separate components as well).
1 Create actions for your list and edit forms that return partialviews - just the parts that are needed for that view (self-contained)
controller:
[HttpGet]
public ActionResult AddCost()
{
var model = new Cost();
return PartialView(model);
}
[HttpPost]
public void AddCost(Cost model)
{
if (ModelState.IsValid) {
db.SaveCost(model);...
}
}
form Views/Home/AddCost.cshtml:
#using (Ajax.BeginForm(...
{
<div class='editor-label'>#Html.LabelFor(model=>model.Description)</div>
...etc...
}
I'll leave you to set the Ajax.BeginForm properties. But make sure the on-success calls reloadCostList() (see below)
controller
public ActionResult CostList()
{
var model = db.loadCosts(); ...
return PartialView(model);
}
list, Views/Home/CostList.cshtml
#model IEnumerable<ViewModels.Cost>
<table>
<thead>
<tr>
<th>Cost Description</th>
...
<tbody>
#foreach (var cost in Model.Costs)
{
<tr data-id='#cost.Id'>
<td>#Html.DisplayFor(x=>cost.Description)</td>
...
<td><a href='#' class='remove-button'>Remove</a></td>
}
...
2 Create an action + view for the main page with placeholder for the form and calls the list partial-action, eg:
<div id="body">
<div id="formWrapper">
#Html.Action("AddCost")
</div>
<div id="listWrapper">
#Html.Action("ListView")
</div>
</div>
if you already load the data for the page, you can pass it directly to the partial, but there's no need:
#Html.Partial("ListView", Model.Costs)
this allows you to refresh the list via an ajax call, something like:
function reloadCostList() {
$(".listWrapper").load("Home/CostList");
}
(ideally, $.ajax and add some fancy UI to indicate loading)
3 Add a remove action to your controller
[HttpPost]
public void RemoveCost(int id)
{
}
4 Wire up the Remove link
$(function() {
$(".remove-button").click(function() {
var id = $(this).closest("tr").attr("id");
$.post("/Home/RemoveCost/" + id, null, function() {
$(".listWrapper").load("Home/CostList");
// or reloadCostList(); from above
// or:
//$(".listWrapper tr[id=" + id + "]").hide();
});
});
}
rather than re-load the entire list, you could just remove the row (add some fancy UI like fade-out...)
Related
I have a grid which includes below hyperlink row,currently for all rows we have same hyperlink and only ID is changing and it is working fine.
<a href=" + #ViewBag.Url+ ID + " target='_blank'>Test</a>
Now for every row, we have different link url which i would get from action method when I pass ID.
I want to call MVC Action Method to get hyperlink url and then open it in another tab.How can I accomplish this?
I tried this one but it is not opening hyperlink?
<div class="row">
<div class="col-md-4">
Click Here;
</div>
</div>
public string GetPDFUrl(string id)
{
return "test.com" + id;
}
There are several ways to solve your problem. one of them is using child actions.
Put your generating URL part into a partial view to put your logic in your action method. So, create a child action method that can only be called in your views.
[ChildActionOnly]
public ActionResult GenerateUrlPartial(int id)
{
var generatedUrl = "";//your url business is here
var model = new UrlInfo { Url = generatedUrl };
return PartialView(model);
}
Then, create GenerateUrlPartial.cshtml partial view :
#model UrlInfo
#{
Layout = null;
ViewBag.Title = "GenerateUrlPartial";
}
<div class="row">
<div class="col-md-4">
Click Here;
</div>
</div>
And in your loop, call the action method like this :
#for (int i = 0; i < 10; i++)
{
Html.RenderAction("GenerateUrlPartial", new { id = i });
}
Hope this helps.
I have Get and Post partial Action. Get take me a list of image which I have in ma app.
[HttpGet]
public PartialViewResult ViewImageFileList()
{
IEnumerable<string> allImages = Directory.EnumerateFiles(Server.MapPath("~/Images/NBAlogoImg/"));
return PartialView(allImages);
}
Post delete image which I extra.
[HttpPost]
public PartialViewResult ViewImageFileList(string imageNameType)
{
var fileToDeletePath = Path.Combine(Server.MapPath("~/Images/NBAlogoImg/"), imageNameType);
if (System.IO.File.Exists(fileToDeletePath))
{
fileOperations.Delete(fileToDeletePath);
}
return PartialView();
}
My .chhtml of my partial view
#model IEnumerable<string>
<div class="name-block-style">
Логотипы которые имеются
</div>
<div id=team-logo-wrapper-images>
<ul>
#foreach (var fullPath in Model)
{
var fileName = Path.GetFileName(fullPath);
<li>
<div class="box-name-image">
<p class="image-name-type">#fileName</p>
<img src="#Url.Content(string.Format("~/Images/NBAlogoImg/{0}", fileName))"
class="logo-images" alt="Логотип команды"
title="Логотип команды" />
</div>
</li>
}
</ul>
<div id="delete-image-form" class="form-group">
#using (Ajax.BeginForm(
"ViewImageFileList",
"Team",
new AjaxOptions() { HttpMethod = "POST", OnComplete = "reloadPage()" }))
{
<label>Введите имя с указание типа изображения</label>
<input type="text" class="form-group" name="imageNameType" id="imageNameType" />
<input type="submit" value="Удалить" class="btn btn-primary" />
}
</div>
<script>
function reloadPage() {
location.reload();
}
</script>
My problem is Null references when I write the deleting image and submit it(i do it by ajax). I have this error Null reference but when I click to continue, the image deleted and my script to reload page work.
I want to understand why I take the null and how I can fix it, because it stops my app always when I delete an image.
The problem is that when you POST after you delete the image you don't populate the model of the partial view, as you do correctly in ViewImageFileList. This has a result when the View Engine try to build the view that you would send after the POST to the client, to get a null reference exception when try to perform the foreach on a null reference.
That being said, the thing you need is to pass to the PartialView all the images. So just add before the return statement in the action method you POST this:
var allImages = Directory.EnumerateFiles(Server.MapPath("~/Images/NBAlogoImg/"));
return PatialView(allImages);
When you browsing images you return view with model passed
return PartialView(allImages); //allImages is a model
But when you deleting images you return view without any model
return PartialView(); //need to pass a model
So after deleting you would like to redirect to ViewImageFileList to browse
all images
[HttpPost]
public RedirectToRouteResult ViewImageFileList(string imageNameType)
{
var fileToDeletePath = Path.Combine(Server.MapPath("~/Images/NBAlogoImg/"), imageNameType);
if (System.IO.File.Exists(fileToDeletePath))
{
fileOperations.Delete(fileToDeletePath);
}
return RedirectToAction("ViewImageFileList");
}
or retrieve images in delete action once again and pass the list to view
[HttpPost]
public PartialViewResult ViewImageFileList(string imageNameType)
{
var fileToDeletePath = Path.Combine(Server.MapPath("~/Images/NBAlogoImg/"), imageNameType);
if (System.IO.File.Exists(fileToDeletePath))
{
fileOperations.Delete(fileToDeletePath);
}
IEnumerable<string> allImages = Directory.EnumerateFiles(Server.MapPath("~/Images/NBAlogoImg/"));
return PartialView(allImages);
}
I would like to create a view that would contain a different view. I've never used json before. How i can do this and How can I format the json data in the view?
My first function "Details" is to retrieve a object from the database and return view "Details.cshtml". In this view I want generates a partial view ("Stats.cshtml"). And now I want to generate a partial view with the data downloaded in the json format inside the Stats function.
Controller
public IActionResult Details(int? id = 1)
{
var person = _context.Persons.Find(id);
return View(champion);
}
public IActionResult Stats()
{
var json = new WebClient().DownloadString("url");
return Json(s);
}
View - Details.cshtml
#model Person
<div class=row">
<div class="col-sm-5"> #Model.Name </div>
<div class="col-sm-5"> #Html.Partial("Stats") </div>
</div>
View - Stats.cshtml
<h2>Stats</h2>
<div> here I want to put in a json field </div>
When I run "Stats" function from the address localhost/Home/Stats I get the result in json, but when I run "Details" function I get view "Details" and "Stats" without the json value.
to render a partial, you have many options, by your code,
the simplest one is: move your Stats code to Details action
public ActionResult Details()
{
...//prepare your person viewModel
var result = new WebClient().DownloadString("url");
var stats = JsonConvert.DeserializeObject<YourViewModel>(result);
//you have 2 options to return data
yourPersonModel.Stats=stats ; //<== you have to change PersonViewModel
//or ViewBag.Stats=stats;
return View(yourPersonModel);
}
then in Details.cshtml:
#Html.Partial("Stats", ViewBag.Stats or Model.Stats)//by your choice before.
Since Html.Action is removed, but ViewComponent comes in Core, you cannot directly call it right now, but this link will tell you how to make it back: #Html.Action in Asp.Net Core
public ActionResult Stats()
{
var result = new WebClient().DownloadString("url");
var yourViewModel = JsonConvert.DeserializeObject<YourViewModel>(result);
return PartialView(yourViewModel);
}
add the following code in your View - Stats.cshtml:
#model YourViewModel
then in Details.cshtml:
#Html.Action("Stats")
Be aware that Html.Action cannot call async actions, be careful to use it.
the next solution is to use new feature ViewComponent, here is details:
https://learn.microsoft.com/en-us/aspnet/core/mvc/views/view-components?view=aspnetcore-2.1
the last one will not be what you expected: use AJAX to load this partial page on page Details is loaded to client
I'm currently having some problems with the following codesnippets which seems almost identical to me, but behaves differently.
These snippets are from two different projects I've been working on, and they are built the same way but only one of them works correctly.
These are the Forms where I enter the controllers:
Form 1, inside a twitter bootstrap dropdown menu, located in the _Layout file:
#using (Html.BeginForm("EditProfile", "ProfilePage", FormMethod.Post))
{
<li>
<button type="submit" class="dropdownButton">Redigera Profil</button>
</li>
}
Form 2, tried different locations but right now it's in a table in a Block view:
<td>
#using (Html.BeginForm("EditProfile", "ProfilePage", FormMethod.Post))
{
<button type="submit">Redigera profil</button>
}
</td>
Both seems pretty identical, right?
Now here are the controllers
Controller 1:
public ActionResult EditProfile(ProfilePage currentPage)
{
var model = new ProfilePageViewModel(currentPage);
model.CurrentUser = ConnectionHelper.GetUserInformationByEmail(User.Identity.Name);
return View("EditProfile", model);
}
Controller 2:
public ActionResult EditProfile(ProfilePage currentPage)
{
ProfilePageViewModel model = new ProfilePageViewModel(currentPage);
model.currentUser = ConnectionHelper.GetCurrentUserByEmail(User.Identity.Name);
return View("EditProfile", model);
}
Also pretty much identical.
I've added the same routing in both projects:
protected override void RegisterRoutes(RouteCollection routes)
{
base.RegisterRoutes(routes);
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" });
}
Now here's the problem:
Form 1 and controller 1 works perfectly and recieves the ProfilePage currentPage without any problems, but form 2 and controller 2 doesn't work and gets null value.
As I stated earlier Form 1 is posted on the _Layout page and Form 2 is posted from a Block which is rendered within an mvc #section. I don't think this is the problem because I've tried to access the controller from different parts of the page, but it's not working anywhere - but in the other project it's working everywhere, which is driving me insane.
Does anyone have any idea why it is like this? I've stepped through both of them while debugging but the only difference is that one works and the other doesn't.
Thanks in advance
deSex
EDIT :
Here I render a section called "content", where almost everything will be rendered.
<div id="content">
#RenderBody()
#RenderSection("content", false)
</div>
My startpage has a ContentArea for blocks, rendered within this section:
#model Intranet.Models.ViewModels.StartPageViewModel
#section content{
#if (User.Identity.IsAuthenticated)
{
<div class="contentArea">
#Html.PropertyFor(x => x.MainContentArea)
</div>
}
}
And here is the controller that inherits from BlockController:
public class ProfileBlockController : BlockController<ProfileBlock>
{
public override ActionResult Index(ProfileBlock currentBlock)
{
ProfileBlockViewModel model;
if (currentBlock != null)
{
model = new ProfileBlockViewModel(currentBlock);
}
else
{
model = (ProfileBlockViewModel)Session["model"];
}
model.CurrentUser = ConnectionHelper.GetCurrentUserByEmail(User.Identity.Name);
var availableStatuses = ConnectionHelper.GetAllOfficeStatuses();
availableStatuses.Remove(model.CurrentUser.OfficeStatus);
model.AvailableStatusChanges = availableStatuses;
Session["model"] = model;
return PartialView(model);
}
}
The "currentPage" route value (i.e. parameter) will only be set by EPiServer's page route. It will always be null in a block controller.
However, you can get the page of the current request in a block controller with:
PageRouteHelper.Page
If the block is being rendered as part of a request for a profile page, you'll be able to get that profile page through PageRouteHelper.
I'm learning MVC3 and building a little "to-do" website as a learning exercise, so I'm open to the idea that I'm just completely going down the wrong path!
Anyway, I have a page working perfectly with regular postbacks. I'm trying to Ajax it up with jQuery and UnobtrusiveAjax and everything still technically works correctly (the data is passed to the controller and saved in my database). The problem is that in the element I replace, each form's fields are all filled with the values that I just passed in on the one form.
Index.cshtml
#model WebUI.Models.HomeViewModel
#{
ViewBag.Title = "Index";
<script src="#Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.unobtrusive-ajax.js")" type="text/javascript"></script>
}
<h2>My Goals</h2>
...snip...
<div id="goal_div">
#foreach (var goal in Model.Goals)
{
Html.RenderPartial("GoalDetail", goal);
}
</div>
GoalDetail.cshtml
#model Domain.Entities.Goal
<div class="goal" id='goal_id#(Model.ID)'>
<h4>#Model.Name</h4>
<p>#DateTime.Now.ToString()</p>
<p class="goal_description">#Model.Progress % complete</p>
<ul>
#foreach (var task in Model.Tasks)
{
using (Ajax.BeginForm("UpdateTask", "Home", new AjaxOptions { UpdateTargetId = "goal_id" + Model.ID }))
{
#Html.HiddenFor(g => g.ID)
#Html.Hidden("TaskID", task.ID)
<li class="task">
#Html.CheckBox("IsComplete", task.IsComplete)
#Html.TextBox("TaskName", task.Name)
#task.Name
<input class="actionButtons" type="submit" value="Update task" />
</li>
}
}
<li>
#using (Html.BeginForm("AddTask", "Home"))
{
#Html.HiddenFor(g => g.ID)
#Html.Editor("TaskName")
<input class="actionButtons" type="submit" value="Add task" />
}
</li>
</ul>
</div>
HomeController.cs
public class HomeController : Controller
{
private IGoalRepository Repository;
public HomeController(IGoalRepository repo)
{
Repository = repo;
}
public ViewResult Index()
{
HomeViewModel viewModel = new HomeViewModel();
viewModel.Goals = Repository.Goals;
return View(viewModel);
}
public ActionResult AddTask(int ID, string TaskName)
{
bool success = Repository.SaveTask(ID, 0, TaskName, DateTime.Today, false);
return RedirectToAction("Index");
}
public ActionResult UpdateTask(int ID, int TaskID, bool IsComplete, string TaskName)
{
bool success = Repository.SaveTask(ID, TaskID, TaskName, DateTime.Today, IsComplete);
Goal updatedGoal = Repository.Goals.FirstOrDefault(g => g.ID == ID);
return PartialView("GoalDetail", updatedGoal);
}
public ActionResult AddGoal(string Name, DateTime Enddate)
{
bool success = Repository.SaveGoal(0, Name, DateTime.Today, Enddate);
return RedirectToAction("Index");
}
public ActionResult UpdateGoal(int GoalID, string Name, DateTime Enddate)
{
bool success = Repository.SaveGoal(GoalID, Name, DateTime.Today, Enddate);
return RedirectToAction("Index");
}
}
I have the time there just to make sure that the AJAX refresh has actually happened, and you'll see why I have the task name there twice.
This is what I see when I first load the page:
Then I check the checkbox of the the 2nd task of the 1st goal, rename it "Updated Task #2", and click the update button. That's when this happens:
Seeing how the task names NOT part of the form are all correct (ignoring the re-ordering for now), and the progress value has been updated correctly (it just takes the completed tasks and divides by the total number of tasks), I have no idea why all the form values have been replaced. Even the AddTask form has been filled in, even though I haven't changed that one to use Ajax yet. I've been searching for reasons for this for 2 days now, and have come up empty.
After even more searching, I finally discovered the issue. Basically, it has to do with the way ModelState works in MVC. Reading this help thread and this article really helped me understand what was happening with my page. I ended up calling ModelState.Clear() in my controller right before returning the partial view, but this SO question and answer
suggests another method.