I have a list of "apps" that users can run. Each app targets a specific API of ours to demonstrate what the API does. Some of these apps require user input so that I can pass user-given parameters into the API.
Each app is responsible for generating the HTML that represents its output. For Apps that do not require any input, it's a straight forward process of executing them in a controller/action from an ajax request, and updating the view with the output.
The challenge is wiring up user input support. I've managed to get 90% of the way there and have hit a roadblock. The apps are responsible for instantiating their own view model. Using a bit of convention, each App has an associated partial view that is located under the same path that the app's namespace is. This lets me create a view model for the app, and return the partial view for each app like this:
public ActionResult GetViewModel(string appId)
{
IApp app = AppFactory.GetAppById(appId);
string path = app.GetType().FullName.Replace('.', '/');
return PartialView($"~/Views/{path}.cshtml", app.CreateViewModel());
}
An example partial view, using an app supplied view model, looks like this:
#using Examples.DataAccess.Query;
#model Query_02_ParameterizedQueryViewModel
#using (#Html.BeginForm("RunAppFromViewModel", "Home", FormMethod.Post))
{
#Html.ValidationSummary(true)
<fieldset>
<div class="form-inline">
<div class="form-group">
#Html.LabelFor(viewModel => viewModel.City)
#Html.EditorFor(viewModel => viewModel.City, new { placeholder = "Phoenix" })
#Html.ValidationMessageFor(viewModel => viewModel.City)
#Html.HiddenFor(viewModel => viewModel.AppId)
</div>
</div>
<button class="btn btn-default" type="submit">Run</button>
</fieldset>
}
The main view has a button that opens a modal bootstrap dialog. When the dialog is opened, I make an ajax request to the server to get the view model and partial view. I then insert the partial view into the modal dialog and update the client-side validation so it works with the unobtrustive stuff. The problem however is that when the form is posted back to the server, and the outputted HTML from the app is returned from the server to the client, I dont know how to update the main view with it.
For example, this is the Main view and the JavaScript that handles both the View Model based Apps and the non-VM based apps.
#using Examples.Browser.ViewModels;
#using Examples.Browser.Models;
#{
ViewBag.Title = "Home Page";
}
#model List<ApiAppsViewModel>
<div class="jumbotron">
<h1>Framework API Micro-Apps</h1>
<p class="lead">Micro-apps provide a way to execute the available APIs in the Framework, without having to write any code, and see the results.</p>
</div>
<div class="container">
<h3 class="text-center">Available API Apps</h3>
<div class="table-responsive">
<table class="table table-hover table-responsive">
<tr>
<th>#nameof(ApiApp.Components)</th>
<th>#nameof(ApiApp.Name)</th>
<th>#nameof(ApiApp.Description)</th>
<th>Options</th>
</tr>
#foreach (ApiAppsViewModel app in this.Model)
{
<tr>
<td>#string.Join(",", app.Components)</td>
<td>#app.Name</td>
<td>#app.Description</td>
<td>
#if (app.IsViewModelRequired)
{
<button type="button"
data-app="#app.Id.ToString()"
data-vm-required="#app.IsViewModelRequired"
data-app-name="#app.Name"
data-toggle="modal"
data-target="#appParameters"
class="btn btn-success">
Run App
</button>
}
else
{
<button type="button"
data-app="#app.Id.ToString()"
data-vm-required="#app.IsViewModelRequired"
class="btn btn-success">
Run App
</button>
}
</td>
</tr>
<tr class="hidden">
<td colspan="4">
<div class="container alert alert-info" data-app="#app.Id.ToString()">
</div>
</td>
</tr>
}
</table>
</div>
</div>
<div class="modal fade"
id="appParameters"
role="dialog"
aria-labelledby="appParametersLabel">
<div class="modal-dialog"
role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<h4 class="modal-title" id="appParametersLabel"></h4>
</div>
<div class="modal-body" id="appDialogBody">
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
$('.btn-success').click(function () {
var button = $(this);
var appId = $(this).data("app");
var vmRequired = $(this).data("vm-required");
if (vmRequired == "False") {
var url = "/Home/RunApp?appId=" + appId;
$.get(url, function (data) {
$("div[data-app='" + appId + "']").html(data);
var buttonColumn = button.parent();
var appRow = buttonColumn.parent();
var hiddenRow = appRow.next()
hiddenRow.removeClass("hidden");
appRow.click(function () {
var hiddenColumn = hiddenRow.children().first();
var resultsDiv = hiddenColumn.children().first();
resultsDiv.empty();
hiddenRow.addClass("hidden");
$(this).off();
hiddenRow.off();
})
hiddenRow.click(function () {
var hiddenColumn = $(this).children().first();
var resultsDiv = hiddenColumn.children().first();
resultsDiv.empty();
$(this).addClass("hidden");
appRow.off();
$(this).off();
})
});
} else {
var appName = $(this).data("app-name");
$('#appParametersLabel').html(appName);
var url = "/Home/GetViewModel?appId=" + appId;
$.get(url, function (data) {
$('#appDialogBody').html(data);
var dialog = $('#appDialogBody');
$.validator.unobtrusive.parse(dialog);
});
$('#appParameters').modal({
keyboard: true,
backdrop: "static",
show: false,
}).on('show', function () {
});
}
});
</script>
When there isn't a view model needed, I stuff the results in an invisible row and make the row visible. Since the View Model apps have their form data submitted from a partial view, when I return the HTML from the controller, it renders it out as raw text to the browser. I assume I can write some java script to handle this but i'm not sure what that would look like. How do I get the form post from the partial view, to return the HTML it generates back to the invisible row within the main view?
This is the controller action that the form posts to, and returns, along with the controller action non-view model based apps use to run their apps and generate the HTML.
[HttpGet]
public async Task<string> RunApp(string appId)
{
IApp app = AppFactory.GetAppById(appId);
if (app == null)
{
return "failed to locate the app.";
}
IAppOutput output = await app.Run();
if (output == null)
{
return "Failed to locate the app.";
}
return output.GetOutput();
}
[HttpPost]
public async Task<string> RunAppFromViewModel(FormCollection viewModelData)
{
IApp app = AppFactory.GetAppById(viewModelData["AppId"]);
foreach(PropertyInfo property in TypePool.GetPropertiesForType(app.ViewModel.GetType()))
{
property.SetValue(app.ViewModel, viewModelData[property.Name]);
}
IAppOutput output = await app.Run();
return output.GetOutput();
}
If you want to update the existing page with the data returned by the RunAppFromViewModel() method, then you need to submit your form using ajax. Since the form is loaded dynamically after the initial page has been loaded, you need to use event delegation. You will also need to store the element you want updated when the form is loaded.
var hiddenRow;
$('.btn-success').click(function () {
// store the element to be updated
hiddenRow = $(this).closest('tr').next('tr').find('.container');
....
});
// Handle the submit event of the form
$('#appDialogBody').on('submit', 'form', function() {
// check if the form is valid
if (!$(this).valid())
{
return;
}
var formData = $(this).serialize(); // serialize the forms controls
var url = '#Url.Action("RunAppFromViewModel", "Home");
$.post(url, formData , function(response) {
hiddenRow.html(response); // assumes your returning html
});
return false; // cancel the default submit
});
Related
I am new to MVC so please bear with me.
I am trying to send a string from a textbox to a controller method so I can find an object in a database. However, I do not know how to send the string successfully from the view to the controller in a HttpGet request (only in HttpPost)
The code in my view
<div>
<label>Email</label>
#Html.TextBox("email")
</div>
<div class="btn btn-success">
#Html.ActionLink("Edit RSVP", "Edit")
</div>
The ViewResult method in my controller
// Problem is the email parameter is always null
[HttpGet]
public ViewResult Edit(string email)
{
// If the email the typed is find, it will display their contents on to a RsvpForm view
return View("RsvpForm", guestRepository.Find(email));
}
Anyone know how I can send this string through, I would be grateful.
Thanks
Like this:
#using (Html.BeginForm("Edit", "ControllerName", FormMethod.Get))
{
<div>
<label>Email</label>
#Html.TextBox("email")
</div>
<div class="btn btn-success">
<input type="submit" value="Edit RSVP" />
</div>
}
Note: I can't tell from your description whether or not you are trying to do this without reloading the page. This option will post the page to the controller, so you will get a page reload.
If you want this to load without posting the page, you can look into Ajax.BeginForm. Here is a StackOverflow article with a decent primer on the AJAX form.
update
For your example, you may could do something like this if you want to use AJAX. This is all untested, but may be close to what you would need.
First you can create a partial view that represents the user data that you want to display:
RsvpForm.cshtml
#model GuestData
<div class="hdr">Name</div>
<div class="value">#Model.Name</div>
<div class="hdr">Email</div>
<div class="value">#Model.Email</div>
Then you want to make sure that your controller returns the partial view based on the email that is sent via the GET:
GuestDataController.cs
[HttpGet]
public ActionResult Edit(string email)
{
// If the email the typed is find, it will display their contents on to a RsvpForm view
return PartialView("RsvpForm", guestRepository.Find(email));
}
Then you create the AJAX form to submit the request via a GET and load the partial view without reloading the page: view.cshtml
<script src="#Url.Content("~/Scripts/jquery.unobtrusive-ajax.js")" type="text/javascript"></script>
#using (Ajax.BeginForm("Edit", "GuestData", null, new AjaxOptions { UpdateTargetId = "UserData", HttpMethod = "Get" }, null))
{
<div>
<label>Email</label>
#Html.TextBox("email")
</div>
<div class="btn btn-success">
<input type="submit" value="Edit RSVP" />
</div>
}
<div id="UserData"></div>
The easiest way to do it is to create a form as follow :
#using(Html.BeginForm("Edit", ControllerName, FormMethod.GET))
{
#Html.Label("Email")
#Html.TextBox("email")
<input type="submit" value="Edit RSVP"/>
}
or you can use Jquery to change the link when textbox value change (which I do not recommend):
$('input[name=email]').on('change' function()
{
var value = $(this).val();
var href = $('.btn').next('a').attr('href');
href += '?email='+value;
$('.btn').next('a').attr('href', href)
});
I have a view partial in a strongly typed controller. Is it possible to render the would-be contents of that view partial on mouseclick?
Example:
Active View
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
<div id="modalView"></div>
<script>
$(document).ready(function () {
$('.open-popup-link').magnificPopup({
key: 'my-popup',
type: 'inline',
inline: {
// Define markup. Class names should match key names.
markup: '<div class="white-popup"><div class="mfp-close"></div>awesome</div>'
}
},
{
callbacks: {
open: function(){
}
}
});
$('.open-popup-link').on('mfpOpen', function(e /*, params */) {
var linkText = // how to I grab this? (e.g. 1, 2, 3, or 4)
$.ajax({
// call view partial withlinktext as parameter
//on success
// var inlineContent = viewPartialContent
// On error
// var inlineCOntent = 'Uh oh, something went wrong'
});
});
});
</script>
View Partial
#model *******.Models.Reservation
<div class="container">
<div class="section-heading">
<h2 class="red">Confirm Your Reservation</h2><br />
</div>
<div class="section-content">
<div class="row">
<h3 class="black text-center">Are you sure you want to reserve space <span class="dark-red">#Model.SpaceNumber</span></h3>
<h4 class="black text-center">for <span class="dark-red">#Model.Game.Description</span> on <span class="dark-red">#Model.Game.Date.ToShortDateString()</span>?</h4>
</div>
<div class="row">
<div class="hero-buttons text-center">
No
<form action="/api/Reservations" method="post" id="confirmationForm">
#Html.Hidden("eRaiderUserName", #Model.eRaiderUserName)
#Html.Hidden("SpaceNumber", #Model.SpaceNumber)
<input type="submit" value="Yes" class="btn btn-red btn-lg white">
</form>
</div>
</div>
</div>
</div>
Method for viewpartial in controller
public ActionResult Confirm(int spaceNumber)
{
var reservation = new Reservation { SpaceNumber=spaceNumber, UserName=AppSettings.CurrentUserName, Game=db.Games.FirstOrDefault(g => g.ID == AppSettings.CurrentGameID) };
return View(reservation);
}
Does this make sense, and can I make it work?
You need to do two things:
change your Confirm method, so that it returns PartialView(reservation) instead of View(reservation)
You need to use AJAX, for example jQuery ajax, to get the HTML, and render it in your page
$.ajax({url = 'the url for your Confirm action',
type = 'GET',
dataType='html',
data = params}).done(function(html) {
// use the jQuery and the html to inject it wherever you need in your page
});
NOTE: params is a jQuery object which contains the data you need to pass like spaceNumber in this case, i.e.
var params = { spaceNumber: 'spaceNumberValue' }
I call a modal which allows a user to select a bunch of dynamically created check boxes and then submit the form to a controller which saves all the info from the FormCollection passed in to it then does a RedirectToAction to the page that calls the modal.
I want to be able to still to save the stuff in the form, but, instead of redirecting to the page that calls the modal i want to stay in the modal.
modal
<script type="text/javascript">
// Close Modal when done.
function CloseModal() {
$("#SkillModalWindow").modal("hide");
}
</script>
#using (Ajax.BeginForm("Save", "SkillGroup", null, new AjaxOptions
{
HttpMethod = "Post",
OnSuccess = "CloseModal"
},
new { id = "CreateSkillGroups" }))
{
#Html.ValidationSummary(true)
#Html.Hidden("JobRoleId", (int)ViewBag.JobRoleID)
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="myModalLabel">Add New Skills to Job Role</h3>
</div>
<div class="modal-body" id="CreateModal">#Html.Partial("_Create")</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
}
Controllor Action
[HttpPost]
public ActionResult Save(FormCollection formCollection)
{
foreach (var key in formCollection.AllKeys)
{
do stuff.....
}
return RedirectToAction("Index");
}
You could return a partial view containing the modal markup instead of redirecting:
[HttpPost]
public ActionResult Save(FormCollection formCollection)
{
foreach (var key in formCollection.AllKeys)
{
do stuff.....
}
return PartialView("_Modal");
}
Also make sure that you have included the jquery.unobtrusive-ajax.js script in your main view if you want the Ajax.BeginForm helper to work.
I'm using MVC 4 and Entity Framework to develop an intranet web application. I have a list of persons which can be modify by an edit action. I wanted to make my app more dynamic by using modal forms. So I tried to put my edit view into my Bootstrap modal and I have 2 questions about it :
Should I use a simple or a partial view?
How can I perform the validation (actually it work but it redirects me to my original view so not in the modal form)
I think I have to use AJAX and/or jQuery but I'm new to these technologies. Any help would be appreciated.
EDIT : My Index View :
#model IEnumerable<BuSIMaterial.Models.Person>
#{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<br />
<div class="group">
<input type="button" value="New person" class="btn" onclick="location.href='#Url.Action("Create")';return false;"/>
<input type="button" value="Download report" class="btn" onclick="location.href='#Url.Action("PersonReport")';return false;"/>
</div>
#using (Html.BeginForm("SelectedPersonDetails", "Person"))
{
<form class="form-search">
<input type="text" id="tbPerson" name="tbPerson" placeholder="Find an employee..." class="input-medium search-query">
<button type="submit" class="btn">Search</button>
</form>
}
<table class="table">
<thead>
<tr>
<th>Firstname</th>
<th>Lastname</th>
<th>Start Date</th>
<th>End Date</th>
<th>Details</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
#foreach (BuSIMaterial.Models.Person item in ViewBag.PageOfPersons)
{
<tr>
<td>#item.FirstName</td>
<td>#item.LastName</td>
<td>#item.StartDate.ToShortDateString()</td>
<td>
#if (item.EndDate.HasValue)
{
#item.EndDate.Value.ToShortDateString()
}
</td>
<td>
<a class="details_link" data-target-id="#item.Id_Person">Details</a>
</td>
<td>
<div>
<button class="btn btn-primary edit-person" data-id="#item.Id_Person">Edit</button>
</div>
</td>
</tr>
<tr>
<td colspan="6">
<table>
<tr>
<th>National Number</th>
<td>#item.NumNat</td>
</tr>
<tr>
<th>Vehicle Category</th>
<td>#item.ProductPackageCategory.Name</td>
</tr>
<tr>
<th>Upgrade</th><td>#item.Upgrade</td>
</tr>
<tr>
<th>House to work</th>
<td>#item.HouseToWorkKilometers.ToString("G29")</td>
</tr>
</table>
<div id="details_#item.Id_Person"></div>
</td>
</tr>
}
</tbody>
</table>
<div class="modal hide fade in" id="edit-member">
<div id="edit-person-container"></div>
</div>
#section Scripts
{
#Scripts.Render("~/bundles/jqueryui")
#Styles.Render("~/Content/themes/base/css")
<script type="text/javascript" language="javascript">
$(document).ready(function () {
$('#tbPerson').autocomplete({
source: '#Url.Action("AutoComplete")'
});
$(".details_link").click(function () {
var id = $(this).data("target-id");
var url = '/ProductAllocation/ListByOwner/' + id;
$("#details_"+ id).load(url);
});
$('.edit-person').click(function () {
var url = "/Person/EditPerson";
var id = $(this).attr('data-id');
$.get(url + '/' + id, function (data) {
$('#edit-person-container').html(data);
$('.edit-person').modal('show');
});
});
});
</script>
}
My Partial View :
#model BuSIMaterial.Models.Person
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="myModalLabel">Edit</h3>
</div>
<div>
#using (Ajax.BeginForm("EditPerson", "Person", FormMethod.Post,
new AjaxOptions
{
InsertionMode = InsertionMode.Replace,
HttpMethod = "POST",
UpdateTargetId = "list-of-people"
}))
{
#Html.ValidationSummary()
#Html.AntiForgeryToken()
<div class="modal-body">
<div class="editor-field">
#Html.TextBoxFor(model => model.FirstName, new { maxlength = 50 })
#Html.ValidationMessageFor(model => model.FirstName)
</div>
<div class="editor-field">
#Html.TextBoxFor(model => model.LastName, new { maxlength = 50 })
#Html.ValidationMessageFor(model => model.LastName)
</div>
</div>
<div class="modal-footer">
<button class="btn btn-inverse" type="submit">Save</button>
</div>
}
You should use partial views. I use the following approach:
Use a view model so you're not passing your domain models to your views:
public class EditPersonViewModel
{
public int Id { get; set; } // this is only used to retrieve record from Db
public string Name { get; set; }
public string Age { get; set; }
}
In your PersonController:
[HttpGet] // this action result returns the partial containing the modal
public ActionResult EditPerson(int id)
{
var viewModel = new EditPersonViewModel();
viewModel.Id = id;
return PartialView("_EditPersonPartial", viewModel);
}
[HttpPost] // this action takes the viewModel from the modal
public ActionResult EditPerson(EditPersonViewModel viewModel)
{
if (ModelState.IsValid)
{
var toUpdate = personRepo.Find(viewModel.Id);
toUpdate.Name = viewModel.Name;
toUpdate.Age = viewModel.Age;
personRepo.InsertOrUpdate(toUpdate);
personRepo.Save();
return View("Index");
}
}
Next create a partial view called _EditPersonPartial. This contains the modal header, body and footer. It also contains the Ajax form. It's strongly typed and takes in our view model.
#model Namespace.ViewModels.EditPersonViewModel
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="myModalLabel">Edit group member</h3>
</div>
<div>
#using (Ajax.BeginForm("EditPerson", "Person", FormMethod.Post,
new AjaxOptions
{
InsertionMode = InsertionMode.Replace,
HttpMethod = "POST",
UpdateTargetId = "list-of-people"
}))
{
#Html.ValidationSummary()
#Html.AntiForgeryToken()
<div class="modal-body">
#Html.Bootstrap().ControlGroup().TextBoxFor(x => x.Name)
#Html.Bootstrap().ControlGroup().TextBoxFor(x => x.Age)
</div>
<div class="modal-footer">
<button class="btn btn-inverse" type="submit">Save</button>
</div>
}
Now somewhere in your application, say another partial _peoplePartial.cshtml etc:
<div>
#foreach(var person in Model.People)
{
<button class="btn btn-primary edit-person" data-id="#person.PersonId">Edit</button>
}
</div>
// this is the modal definition
<div class="modal hide fade in" id="edit-person">
<div id="edit-person-container"></div>
</div>
<script type="text/javascript">
$(document).ready(function () {
$('.edit-person').click(function () {
var url = "/Person/EditPerson"; // the url to the controller
var id = $(this).attr('data-id'); // the id that's given to each button in the list
$.get(url + '/' + id, function (data) {
$('#edit-person-container').html(data);
$('#edit-person').modal('show');
});
});
});
</script>
I prefer to avoid using Ajax.BeginForm helper and do an Ajax call with JQuery. In my experience it is easier to maintain code written like this. So below are the details:
Models
public class ManagePeopleModel
{
public List<PersonModel> People { get; set; }
... any other properties
}
public class PersonModel
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
... any other properties
}
Parent View
This view contains the following things:
records of people to iterate through
an empty div that will be populated with a modal when a Person needs to be edited
some JavaScript handling all ajax calls
#model ManagePeopleModel
<h1>Manage People</h1>
#using(var table = Html.Bootstrap().Begin(new Table()))
{
foreach(var person in Model.People)
{
<tr>
<td>#person.Id</td>
<td>#Person.Name</td>
<td>#person.Age</td>
<td>#html.Bootstrap().Button().Text("Edit Person").Data(new { #id = person.Id }).Class("btn-trigger-modal")</td>
</tr>
}
}
#using (var m = Html.Bootstrap().Begin(new Modal().Id("modal-person")))
{
}
#section Scripts
{
<script type="text/javascript">
// Handle "Edit Person" button click.
// This will make an ajax call, get information for person,
// put it all in the modal and display it
$(document).on('click', '.btn-trigger-modal', function(){
var personId = $(this).data('id');
$.ajax({
url: '/[WhateverControllerName]/GetPersonInfo',
type: 'GET',
data: { id: personId },
success: function(data){
var m = $('#modal-person');
m.find('.modal-content').html(data);
m.modal('show');
}
});
});
// Handle submitting of new information for Person.
// This will attempt to save new info
// If save was successful, it will close the Modal and reload page to see updated info
// Otherwise it will only reload contents of the Modal
$(document).on('click', '#btn-person-submit', function() {
var self = $(this);
$.ajax({
url: '/[WhateverControllerName]/UpdatePersonInfo',
type: 'POST',
data: self.closest('form').serialize(),
success: function(data) {
if(data.success == true) {
$('#modal-person').modal('hide');
location.reload(false)
} else {
$('#modal-person').html(data);
}
}
});
});
</script>
}
Partial View
This view contains a modal that will be populated with information about person.
#model PersonModel
#{
// get modal helper
var modal = Html.Bootstrap().Misc().GetBuilderFor(new Modal());
}
#modal.Header("Edit Person")
#using (var f = Html.Bootstrap.Begin(new Form()))
{
using (modal.BeginBody())
{
#Html.HiddenFor(x => x.Id)
#f.ControlGroup().TextBoxFor(x => x.Name)
#f.ControlGroup().TextBoxFor(x => x.Age)
}
using (modal.BeginFooter())
{
// if needed, add here #Html.Bootstrap().ValidationSummary()
#:#Html.Bootstrap().Button().Text("Save").Id("btn-person-submit")
#Html.Bootstrap().Button().Text("Close").Data(new { dismiss = "modal" })
}
}
Controller Actions
public ActionResult GetPersonInfo(int id)
{
var model = db.GetPerson(id); // get your person however you need
return PartialView("[Partial View Name]", model)
}
public ActionResult UpdatePersonInfo(PersonModel model)
{
if(ModelState.IsValid)
{
db.UpdatePerson(model); // update person however you need
return Json(new { success = true });
}
// else
return PartialView("[Partial View Name]", model);
}
In reply to Dimitrys answer but using Ajax.BeginForm the following works at least with MVC >5 (4 not tested).
write a model as shown in the other answers,
In the "parent view" you will probably use a table to show the data.
Model should be an ienumerable. I assume, the model has an id-property. Howeverm below the template, a placeholder for the modal and corresponding javascript
<table>
#foreach (var item in Model)
{
<tr> <td id="editor-success-#item.Id">
#Html.Partial("dataRowView", item)
</td> </tr>
}
</table>
<div class="modal fade" id="editor-container" tabindex="-1"
role="dialog" aria-labelledby="editor-title">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="editor-content-container"></div>
</div>
</div>
<script type="text/javascript">
$(function () {
$('.editor-container').click(function () {
var url = "/area/controller/MyEditAction";
var id = $(this).attr('data-id');
$.get(url + '/' + id, function (data) {
$('#editor-content-container').html(data);
$('#editor-container').modal('show');
});
});
});
function success(data,status,xhr) {
$('#editor-container').modal('hide');
$('#editor-content-container').html("");
}
function failure(xhr,status,error) {
$('#editor-content-container').html(xhr.responseText);
$('#editor-container').modal('show');
}
</script>
note the "editor-success-id" in data table rows.
The dataRowView is a partial containing the presentation of an model's item.
#model ModelView
#{
var item = Model;
}
<div class="row">
// some data
<button type="button" class="btn btn-danger editor-container" data-id="#item.Id">Edit</button>
</div>
Write the partial view that is called by clicking on row's button (via JS $('.editor-container').click(function () ... ).
#model Model
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<h4 class="modal-title" id="editor-title">Title</h4>
</div>
#using (Ajax.BeginForm("MyEditAction", "Controller", FormMethod.Post,
new AjaxOptions
{
InsertionMode = InsertionMode.Replace,
HttpMethod = "POST",
UpdateTargetId = "editor-success-" + #Model.Id,
OnSuccess = "success",
OnFailure = "failure",
}))
{
#Html.ValidationSummary()
#Html.AntiForgeryToken()
#Html.HiddenFor(model => model.Id)
<div class="modal-body">
<div class="form-horizontal">
// Models input fields
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
}
This is where magic happens: in AjaxOptions, UpdateTargetId will replace the data row after editing, onfailure and onsuccess will control the modal.
This is, the modal will only close when editing was successful and there have been no errors, otherwise the modal will be displayed after the ajax-posting to display error messages, e.g. the validation summary.
But how to get ajaxform to know if there is an error? This is the controller part, just change response.statuscode as below in step 5:
the corresponding controller action method for the partial edit modal
[HttpGet]
public async Task<ActionResult> EditPartData(Guid? id)
{
// Find the data row and return the edit form
Model input = await db.Models.FindAsync(id);
return PartialView("EditModel", input);
}
[HttpPost, ValidateAntiForgeryToken]
public async Task<ActionResult> MyEditAction([Bind(Include =
"Id,Fields,...")] ModelView input)
{
if (TryValidateModel(input))
{
// save changes, return new data row
// status code is something in 200-range
db.Entry(input).State = EntityState.Modified;
await db.SaveChangesAsync();
return PartialView("dataRowView", (ModelView)input);
}
// set the "error status code" that will redisplay the modal
Response.StatusCode = 400;
// and return the edit form, that will be displayed as a
// modal again - including the modelstate errors!
return PartialView("EditModel", (Model)input);
}
This way, if an error occurs while editing Model data in a modal window, the error will be displayed in the modal with validationsummary methods of MVC; but if changes were committed successfully, the modified data table will be displayed and the modal window disappears.
Note: you get ajaxoptions working, you need to tell your bundles configuration to bind jquery.unobtrusive-ajax.js (may be installed by NuGet):
bundles.Add(new ScriptBundle("~/bundles/jqueryajax").Include(
"~/Scripts/jquery.unobtrusive-ajax.js"));
In $('.editor-container').click(function (){}), shouldn't var url = "/area/controller/MyEditAction"; be var url = "/area/controller/EditPartData";?
I'm currently using Twitter's Bootstrap toolkit on a new project and I had a question on the best way to use the modal dialog in ASP.NET MVC3.
Is the best practice to have a Partial that contains the modal's markup and then use javascript to render that onto the page or is there a better approach?
Here goes my little tutorial which demonstrates Twitter's Bootstrap (2.x) modal dialog that works with forms and partials in ASP.Net MVC 4.
To download similar project but targeting MVC 5.1 and Bootstrap 3.1.1 please visit this site.
Start with an empty MVC 4 Internet template.
Add reference to Bootstrap using NuGet
In the App_Start/BundleConfig.cs add the following lines:
bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include("~/Scripts/bootstrap.js"));
bundles.Add(new StyleBundle("~/Content/bootstrap").Include(
"~/Content/bootstrap.css",
"~/Content/bootstrap-responsive.css"));
In the Views/Shared/_Layout.cshtml
modify the #styles.Render line so it will look like:
#Styles.Render("~/Content/css", "~/Content/themes/base/css", "~/Content/bootstrap")
and the #Scripts.Render line:
#Scripts.Render("~/bundles/jquery", "~/bundles/jqueryui", "~/bundles/bootstrap")
So far we have Bootstrap prepared to work with MVC 4 so let's add a simple model class MyViewModel.cs to the /Models folder:
using System.ComponentModel.DataAnnotations;
namespace MvcApplication1.Models
{
public class MyViewModel
{
public string Foo { get; set; }
[Required(ErrorMessage = "The bar is absolutely required")]
public string Bar { get; set; }
}
}
In the HomeController Add the following lines:
using MvcApplication1.Models;
//...
public ActionResult Create()
{
return PartialView("_Create");
}
[HttpPost]
public ActionResult Create(MyViewModel model)
{
if (ModelState.IsValid)
{
try
{
SaveChanges(model);
return Json(new { success = true });
}
catch (Exception e)
{
ModelState.AddModelError("", e.Message);
}
}
//Something bad happened
return PartialView("_Create", model);
}
static void SaveChanges(MyViewModel model)
{
// Uncommment next line to demonstrate errors in modal
//throw new Exception("Error test");
}
Create new Partial View in the Views/Home folder and name it _Create.cshtml:
#using MvcApplication1.Models
#model MyViewModel
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="myModalLabel">Create Foo Bar</h3>
</div>
#using (Html.BeginForm("Create", "Home", FormMethod.Post, new { #class = "modal-form" }))
{
#Html.ValidationSummary()
<div class="modal-body">
<div>
#Html.LabelFor(x => x.Foo)
#Html.EditorFor(x => x.Foo)
#Html.ValidationMessageFor(x => x.Foo)
</div>
<div>
#Html.LabelFor(x => x.Bar)
#Html.EditorFor(x => x.Bar)
#Html.ValidationMessageFor(x => x.Bar)
</div>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal" aria-hidden="true">Undo</button>
<button class="btn btn-primary" type="submit">Save</button>
</div>
}
In the Home/Index.cshtml remove the default content from the template and replace it with following:
#{
ViewBag.Title = "Home Page";
}
<br />
<br />
<br />
#Html.ActionLink("Create", "Create", null, null, new { id = "btnCreate", #class = "btn btn-small btn-info" })
<div id='dialogDiv' class='modal hide fade in'>
<div id='dialogContent'></div>
</div>
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
<script type="text/javascript">
$(function () {
//Optional: turn the chache off
$.ajaxSetup({ cache: false });
$('#btnCreate').click(function () {
$('#dialogContent').load(this.href, function () {
$('#dialogDiv').modal({
backdrop: 'static',
keyboard: true
}, 'show');
bindForm(this);
});
return false;
});
});
function bindForm(dialog) {
$('form', dialog).submit(function () {
$.ajax({
url: this.action,
type: this.method,
data: $(this).serialize(),
success: function (result) {
if (result.success) {
$('#dialogDiv').modal('hide');
// Refresh:
// location.reload();
} else {
$('#dialogContent').html(result);
bindForm();
}
}
});
return false;
});
}
</script>
}
If you run your application, a nice Bootstrap modal will appear after clicking the Create button on the Home page.
Try to uncomment the SaveChanges() //throw line in HomeController.cs to prove that your controller handled errors will appear correctly in the dialog.
I hope that my sample clarifies a bit whole process of incorporating Bootstrap and creating modals in the MVC application.
Great example, I had to modify slightly for MVC 5 and Bootstrap 3.3.7, I changed the target div tags to the following, otherwise I was just getting the grey background and no modal dialog. Hope this helps someone.
<div id='dialogDiv' class='modal fade' tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div id='dialogContent'></div>
</div>
</div>
</div>
Thanks #zjerry the solution is great but jQuery validation does not work, in order to fix you need to change the function bindForm as follow:
function bindForm(dialog) {
$.validator.unobtrusive.parse('form');
$('form', dialog).submit(function () {
$.ajax({
url: this.action,
type: this.method,
data: $(this).serialize(),
success: function (result) {
if (result.success) {
$('#dialogDiv').modal('hide');
// Refresh:
// location.reload();
} else {
$('#dialogContent').html(result);
bindForm();
}
}
});
return false;
});
}
Notice the first line of the function, because the form is loaded after jQuery validation was initialized.
Many thanks
This really depends on your design, but you should have a template for the modals.
On a single web app for example, you should have a template lying around that you would create a new instance from each time.
Usually on a normal website you would want to store this template inside a js creation function, so that you won't have to send the file to the user each time via http, and they can have it cached.