RenderAction() to make decoupled components but how to communicate with other components - asp.net-mvc

I have a busy view where most of the sections on there work off an ID. I'm looking for a more component way to handle each section so I'm using RenderAction() for each section where they have their own controllers. However I have a search section/"component" and when they put in a new Id and submit on that section/"component", I need a way for that to communicate to all the other RenderActions() that new Id so they can do their thing (query DB to get more info specific to that section).
My Search section would be something like:
public class SearchController : Controller
{
[HttpGet]
public ActionResult SearchContract()
{
var vm = new SearchVM();
return PartialView(vm);
}
[HttpPost]
public ActionResult SearchContract(SearchVM Search)
{
return PartialView(Search);
}
}
#using (Html.BeginForm())
{
<div class="row">
<div class="col-md-3">Contract Id</div>
<div class="col-md-6">
#Html.TextBoxFor(m => m.Id, new { #class = "form-control" })
</div>
</div>
<input type="submit" />
}
Let's say ContractHeader is a section/"component" using RenderAction() that hits a different controller and method from the search:
public class ContractController : Controller
{
public ActionResult ContractHeader(int ContractId)
{
// query contracts
return PartialView(vm);
}
}
Again, I'm looking for a more component oriented way with this. Yes it could all be in one controller but that's not what I'm looking for here. I want a more decoupled/compartmentalized approach to these areas on my views but trying to figure out how they can communicate with each other when "events" happen.

I think I have it figured out. Basically on each search "component" (I'm calling components a separate controller and view that you use RenderAction() to get on your main view) the method that gets called when the search button is pressed will return the following code (I subclassed Controller and put tis method in)
public ActionResult RedirectWithQueryString()
{
// get the referrer url without the old query string (which will be the main view)
var uri = new Uri(Request.UrlReferrer.ToString());
var url = Request.UrlReferrer.ToString().Replace(uri.Query, "");
var allQS = System.Web.HttpUtility.ParseQueryString(uri.Query);
var currentQS = System.Web.HttpUtility.ParseQueryString(Request.Url.Query);
var combinedQS = new NameValueCollection();
// update existing values
foreach (var key in allQS.AllKeys)
{
combinedQS.Add(key, allQS[key]);
}
// add new values
foreach (var key in currentQS.AllKeys)
{
if (combinedQS.AllKeys.Contains(key))
combinedQS[key] = currentQS[key];
else
combinedQS.Add(key, currentQS[key]);
}
var finalUrl = url + combinedQS.ToQueryString();
return Redirect(finalUrl);
}
public class ContractSearchController : MyBaseController
{
// GET: ContractSearch
public ActionResult Index(ContractSearchVM model)
{
return PartialView("ContractSearch", model);
}
public ActionResult SearchContracts(ContractSearchVM model)
{
return RedirectWithQueryString();
}
}
public class StopsSearchController : MyBaseController
{
public ActionResult Index(StopsSearchVM model)
{
// query to get some search related reference data like states list for drop down
return PartialView("StopsSearch", model);
}
public ActionResult SearchStops(StopsSearchVM model)
{
return RedirectWithQueryString();
}
}
SearchContracts() and SearchStops() methods are called from their own forms in their own views using HttpGet. In those methods then we are provided with just that forms query string but we also can get the UrlReferrer query string which will have other search forms key/values in it. So RedirectWithQueryString() basically makes sure the final query string has ALL keys required to satisfy the model binding of any search components on the view and will update the given keys with the current value for the current search component that the submit button was on.
So this then causes us to refresh to the current view with all current key/values in query string for all search components which then is calling all the RenderActions() and the values can be passed.
#model FocusFridayComponents.Models.CombinedVM
<div class="row">
<div class="col-md-6">
<div class="row">
<div class="col-md-12">
#{ Html.RenderAction("Index", "ContractSearch"); }
</div>
</div>
<div class="row">
<div class="col-md-12">
#{ Html.RenderAction("Index", "StopsSearch"); }
</div>
</div>
</div>
<div class="col-md-6">
<!-- Contract Header -->
<div class="row">
<div class="col-md-12">
#{ Html.RenderAction("Header", "ContractHeader", new { ContractId = Model.ContractSearch.Id }); }
</div>
</div>
<!-- Contract Routes -->
<div class="row">
<div class="col-md-12">
#* #{ Html.RenderAction("Index", "ContractRoutes", new { ContractId = Model.Id }); } *#
</div>
</div>
</div>
In the main view you're working on you just make a VM that combines the search VM's you're using on the view. The model binding will correctly map the query string keys that match the search VM's even when they are inside the combined VM. The catch here would be to make sure the keys/props of each search VM don't share the same names of any kind.
What's interesting is for the RenderAction() for contract and stops search I don't need to pass the model into it. The binding just does this automatically. For ContractHeader and ContractRoutes I am passing in a parameter because the idea is those are separate components and have their own input requirements and those can be named completely separate from any search models you may be using in your view so the binding wouldn't be able to map anything. This is a good thing though as it decouples your actual view components from your search components.
So you would do all of this to get components that are decoupled from each other but can still talk to each other and you can assemble your views and reuse a lot of these components by just gluing the RenderAction() parameters between them. This can help reduce giant monolithic VM's that tend to pop up on complex views you're making.

Related

How to load partial razor page into razor view

I am trying to replace my view components with razor pages but it seems that it's not possible to load a partial razor page because a model is expected to be passed yet it is my understanding that the model for a razor page should be declared in the OnGetAsync method. Here is my code...
Razor Page
#page "{id:int}"
#model _BackgroundModel
<form method="POST">
<div>Name: <input asp-for="Description" /></div>
<input type="submit" />
</form>
Razor Page Code-Behind
public class _BackgroundModel : PageModel
{
private readonly IDataClient _dataClient;
public _BackgroundModel(IDataClient dataClient)
{
_dataClient = dataClient;
}
[BindProperty]
public BackgroundDataModel Background { get; set; }
public async Task OnGetAsync(int id)
{
Background = await _dataClient.GetBackground(id);
}
public async Task OnPostAsync()
{
if (ModelState.IsValid)
{
await _dataClient.PostBackground(Background);
}
}
}
Razor View
<div class="tab-pane fade" id="client-background-tab">
<div class="row">
<div class="col-sm-12">
#await Html.PartialAsync("/Pages/Client/_Background.cshtml", new { id = 1 })
</div>
</div>
</div>
Page Load Error
InvalidOperationException: The model item passed into the
ViewDataDictionary is of type '<>f__AnonymousType0`1[System.Int32]',
but this ViewDataDictionary instance requires a model item of type
'WebApp.Pages.Client._BackgroundModel'
In this example (as per MS recommended approach in their docs) the model is set inside the OnGetAsync method which should be run when the page is requested. I have also tried #await Html.RenderPartialAsync("/Pages/Client/_Background.cshtml", new { id = 1 }) but the same error result.
How can I load the razor page into my existing view?
Microsoft confirmed this cannot be achieved and therefore razor pages cannot be used as a replacement for view components.
See the comments of their docs...
MS docs
#RickAndMSFT moderator15 hours ago
#OjM You can redirect to the page, or you can make the core view >code into a partial and call it from both.
Pages are not a replacement for partials or View Components.

Adding multiple items to a list without postback

My model has a list property and in the view i need to be able to add an unlimited number of strings to it.
So far it's not working and my lousy idea to make it work is the following: Each time a string is added, there's a postback. The new string is in the ViewModel's "newString" property (not a list). The HttpPost method will then save "newString" to the database, refill the list "allStrings" with all strings stored in the database and return the view with all strings and an emtpy textbox to add another string.
This is not a good solution for me because:
There's a lot of postbacks if the user wants to add multiple strings
If the user adds some strings to his item (a supplier), all these strings are saved to the database. When he then decides he doesn't want to save the supplier all the stored strings are useless and need to be deleted from the database.
I have not implemented this because I know there's far better solutions and I just don't find them. This is what I have:
The ViewModel:
public class SupplierViewModel
{
public Supplier Supplier { get; set; }
public List<string> allStrings;
public string newString { get; set; }
}
The Controller:
[HttpPost]
public ActionResult Create(SupplierViewModel model)
{
model.allStrings.Add(model.newString);
if (ModelState.IsValid && model.newString == "")
db.Suppliers.Add(model.Supplier);
db.SaveChanges();
return RedirectToAction("Index");
}
model.newString = "";
return View(model);
}
The View:
<div class="editor-label">
#Html.LabelFor(model => model.allStrings)
</div>
#for (int i = 0; i < Model.allStrings.Count; i++)
{
<div class="editor-label">
#Html.EditorFor(model => model.allStrings[i])
</div>
}
<div class="editor-label">
#Html.EditorFor(model => model.newString)
</div>
Note that in this implemented version, none of the strings are saved to the database and the list is cleared after each postback. Only one string (the last one added) is displayed on the view.
Basically the question is: How can I have the user add as many strings as he wants with as few postbacks and database-interaction as possible?
Thanks in advance
You can dynamically add new elements with jquery that will post back to your collection. The html your generating for the textboxes will be similar to
<input type="text" name="allStrings[0]" .../>
<input type="text" name="allStrings[1]" .../>
The name attribute includes an indexer which allows the DefaultModelBinder to bind a collection.
Wrap you textboxes in a container, include a button to add a new item, an input that gets copies and added to the DOM.
<div id="strings">
#for (int i = 0; i < Model.allStrings.Count; i++)
{
<div class="editor-label">
#Html.TextBoxFor(m => m.allStrings[i])
</div>
}
</div>
<div id="newstring" style="display:none;">
<input type="text" name="allStrings[#]" />
</div>
<button type="button" id="addstring">Add</button>
Script
var container = $('#strings');
$('#addstring').click(function() {
var index = container.children('input').length;
var clone = $('#newstring').clone();
clone.html($(clone).html().replace(/\[#\]/g, '[' + index + ']'));
container .append(clone.html());
});
Refer this fiddle for a working example
Note your model no longer required the public string newString { get; set; } property, and when you post back your collection will contain all the values of the textboxes.

One variable in model becomes null after http post

I'm having a problem with my model losing one of its values when submitted in a http post form.
The controller methods:
[HttpGet]
public ActionResult AddTeam(int id)
{
return PartialView("_AddTeam", ViewModelGenerator.TeamFormModel(id));
}
[HttpPost]
public ActionResult AddTeam(TeamFormModel model)
{
TournamentTeamService.CreateTeam(model);
return RedirectToAction("Index", "Tournament", null);
}
The form model:
public class TeamFormModel
{
public TeamViewModel Team { get; set; }
public TournamentViewModel Tournament { get; set; }
public List<PlayerViewModel> Players { get; set; }
}
This function is called from the controller, it creates a new instance of the form model and gets the Tournament from the database and puts it into a view model
public static TeamFormModel TeamFormModel(int id)
{
var _db = new DbContext();
var model = new TeamFormModel();
var tempModel = new TournamentViewModel();
var temp = (from t in _db.tournament
where t.Id == id
select t).SingleOrDefault();
tempModel.Id = temp.Id;
tempModel.Name = temp.Name;
tempModel.SignupStartDate = temp.SignupStartDate;
tempModel.SignupEndDate = temp.SignupEndDate;
tempModel.StartDate = temp.StartDate;
tempModel.EndDate = temp.EndDate;
tempModel.Description = temp.description;
tempModel.TournamentType = temp.TournamentType;
model.Players = new List<PlayerViewModel>();
model.Tournament = tempModel;
return model;
}
Here's the view, the code with the inputs for the player list is missing, it's because it is appendedTo the player-forms div via jquery and it works perfectly so i didn't consider it relevant in this case.
#model Context.ViewModels.TeamFormModel
<script src="~/Scripts/jquery-1.8.2.min.js"></script>
<script src="~/Scripts/jquery.validate.min.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
#using (Html.BeginForm("AddTeam", "Tournament", FormMethod.Post, new { enctype = "multipart/form-data", #class = "form-horizontal" }))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<h2>Registering a team to #Model.Tournament.Name </h2>
<div class="control-group">
#Html.LabelFor(model => model.Team.Name)
<div class="control">
#Html.EditorFor(model => model.Team.Name)
</div>
<a class="add-player-form" href="javascript:void(0)">Add a player</a>
<div id="player-forms">
</div>
</div>
<div class="control-group">
<div class="control" style="clear: both;">
<input type="submit" value="Register" />
</div>
</div>
}
Before i submit this form, the TeamFormModel is perfectly set up, all values are inserted and i only need to save everything to DB. However when it goes back to the controller, the tournament variable in TeamFormModel is null. So when i send it to a function that changes it to a Db model and submits it to db fails everytime.
I know there are ways around the problem like only keeping the id of the tournament instead of the whole model and then get it from db after the form is submitted but it really bugs me that it behaves this way.
I wasn't able to find anything on this specific issue here on stackoverflow, there were a few similar questions but nothing that presented it self the same way.
you have to set something like this
#Html.HiddenFor(x => x.TournmentID);
something has to retain the value of the model that was passed into the view while the view is rendered, this will pass the value back to the controller on post

rename mvc model object in Razor View

I'm using MVC 4 and usually Visual Studio will create all the views for you. I have one form that just has one field and I want to just embed the create form into the Index View.
So the Index View has something like #model IEnumerable<Models.LinkModel>
So I access it by iterating through the Model collection.
But if I try to embed the form for the create action I need #model Models.LinkModel
and it is accessed by Model.Name as well. Is there a way to do this or use a different variable name?
Ok here is some extra info.
SO I have a model.
public class LinkModel
{
public string LinkUrl {get;set;}
}
I have a controller that has the Create and Index ActionResults.
Now in the Index view I have
#model IEnumerable<Models.LinkModel>
#{
ViewBag.Title = "Links";
}
I can do all my fancy logic to list all the links.
#foreach(link in Model)
{
<p>link.LinkUrl<p>
}
The Create View has this
#model Models.LinkModel // Note that it is just one item not IEnumerable
#{
ViewBag.Title = "Add Link";
}
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<fieldset class="editor-fieldset">
<legend>LinkModel</legend>
<div class="editor-label">
#Html.LabelFor(model => model.LinkUrl)
</div>
<div class="editor-field">
#Html.TextBoxFor(model => model.LinkUrl)
</div>
<p>
<input type="submit" value="Add Link" />
</p>
</fieldset>
}
Now it seems pretty stupid to have a create form for just one field. I want to put this form on the Index page. Problem is that I access the object using the variable Model. I wanted to know if there is a way to have two seperate instances or be able to access the Model objects with different names.
Have a composite model with a list of items and 1 single item for the create
public class IndexModel {
public LinkModel CreateModel {get; set;}
public IEnumerable<LinkModel> Items {get; set;}
}
#model IndexModel
#using(Html.BeginForm("create")) {
#Html.EditorFor(m => m.CreateModel.Name);
}
#foreach(var item in Model.Items) {
#item.Name
}

ASP.NET MVC Paging for a search form

I've read several different posts on paging w/ in MVC but none describe a scenario where I have something like a search form and then want to display the results of the search criteria (with paging) beneath the form once the user clicks submit.
My problem is that, the paging solution I'm using will create <a href="..."> links that will pass the desired page like so: http://mysite.com/search/2/ and while that's all fine and dandy, I don't have the results of the query being sent to the db in memory or anything so I need to query the DB again.
If the results are handled by the POST controller action for /Search and the first page of the data is rendered as such, how do I get the same results (based on the form criteria specified by the user) when the user clicks to move to page 2?
Some javascript voodoo? Leverage Session State? Make my GET controller action have the same variables expected by the search criteria (but optional), when the GET action is called, instantiate a FormCollection instance, populate it and pass it to the POST action method (there-by satisfying DRY)?
Can someone point me in the right direction for this scenario or provide examples that have been implemented in the past? Thanks!
My method is to have an Action that handles both the post and the get scenarios.
This is my which can be handled by both GET and POST methods:
public ViewResult Index([DefaultValue(1)] int page,
[DefaultValue(30)] int pageSize,
string search,
[DefaultValue(0)] int regionId,
[DefaultValue(0)] int eventTypeId,
DateTime? from,
DateTime? to)
{
var events = EventRepo.GetFilteredEvents(page, pageSize, search, regionId, eventTypeId, from, to);
var eventFilterForm = EventService.GetEventFilterForm(from, to);
var eventIndexModel = new EventIndexModel(events, eventFilterForm);
return View("Index", eventIndexModel);
}
The eventFilterForm is a presentation model that contains some IEnumerable<SelectListItem> properties for my search form.
The eventIndexModel is a presentation model that combines the eventFilterForm and the results of the search - events
The events is a special type of IPagedList. You can get more information and code for that here and here. The first link talks about IPagedList where as the second link has an Advanced Paging scenario which you should need.
The advanced paging has the following method that I use:
public static string Pager(this HtmlHelper htmlHelper, int pageSize, int currentPage, int totalItemCount, RouteValueDictionary valuesDictionary)
And I use it like so:
<%= Html.Pager(Model.Events.PageSize,
Model.Events.PageNumber,
Model.Events.TotalItemCount,
new
{
action = "index",
controller = "search",
search = ViewData.EvalWithModelState("Search"),
regionId = ViewData.EvalWithModelState("RegionId"),
eventTypeId = ViewData.EvalWithModelState("EventTypeId"),
from = ViewData.EvalDateWithModelState("From"),
to = ViewData.EvalDateWithModelState("To")
}) %>
This creates links that look like:
/event/search?regionId=4&eventTypeId=39&from=2009/09/01&to=2010/08/31&page=3
HTHs,
Charles
Ps. EvalWithModelState is below:
PPs. If you are going to put dates into get variables - I would recommend reading my blog post on it... :-)
/// <summary>
/// Will get the specified key from ViewData. It will first look in ModelState
/// and if it's not found in there, it'll call ViewData.Eval(string key)
/// </summary>
/// <param name="viewData">ViewDataDictionary object</param>
/// <param name="key">Key to search the dictionary</param>
/// <returns>Value in ModelState if it finds one or calls ViewData.Eval()</returns>
public static string EvalWithModelState(this ViewDataDictionary viewData, string key)
{
if (viewData.ModelState.ContainsKey(key))
return viewData.ModelState[key].Value.AttemptedValue;
return (viewData.Eval(key) != null) ? viewData.Eval(key).ToString() : string.Empty;
}
Make the Search parameter part of your View Model:
public SearchViewModel
{
string SearchParameters { get; set; }
List<SearchObjects> SearchResults { get;set; }
}
Then just set the Search Textbox equal to SearchParameters.
You cannot "store" the search query unless you bring back ALL results and then store those in the page somehow. That is horribly inefficient. The web is stateless, so you will have to go back to the database and re-query for more results.
I understand what you are saying; you could change the form to use buttons and post the page back everytime. Or, you could pass all the criteria in the URL for the paging as querystring variables. Or you could use JQuery to do the post (it has a $.post method that can be invoked from a link click or other click (http://api.jquery.com/jQuery.post/).
HTH.
This problem goes away if you include the search text, as well as the current results page, in your querystring instead of POSTing the search text. As an added benefit, your users can then bookmark their search results.
To do this your search button just needs to build the GET request URL using the current value of the search box. This can be done either in javascript or by using GET as your search form's method attribute, e.g. <form method="get" action="/search">.
I recommend cacheing your search results and giving them an ID. Then for each paging link, you can reference the search ID as a parameter (on each search page link) and in your action, pull it from cache, then query over it.
Using this method, you don't need to worry about anything other than the first POST submit of the search form.
Refer to my post for more details.
I had this same problem and here's what I did.
Download PagedList from Nuget
Change your form to do a GET and create a ViewModel type similiar to this (if you love AdventureWorks and Model Binding as much as I do):
`
using PagedList;
namespace SearchFormResultPagingExample.Models {
public class SearchViewModel {
public int? Page { get; set; }
public string EmailAddress { get; set; }
public string LastName { get; set; }
public IPagedList<Contact> SearchResults { get; set; }
public string SearchButton { get; set; }
}
}
`
3.Use the ViewModel as the parameter to your controller's action method
using System.Linq;
using System.Web.Mvc;
using SearchFormResultPagingExample.Models;
using PagedList; //NOTE: use Nuget to reference PagedList
namespace SearchFormResultPagingExample.Controllers {
public class SearchController : Controller {
const int RecordsPerPage = 25;
public ActionResult Index(SearchViewModel model) {
if (!string.IsNullOrEmpty(model.SearchButton) || model.Page.HasValue) {
var entities = new AdventureWorksEntities();
var results = entities.Contacts.Where(c => c.LastName.StartsWith(model.LastName) && c.EmailAddress.StartsWith(model.EmailAddress))
.OrderBy(o => o.LastName);
var pageIndex = model.Page ?? 0;
model.SearchResults = results.ToPagedList(pageIndex, 25);
}
return View(model);
}
}
}
Use the pager on in your View:
#model SearchFormResultPagingExample.Models.SearchViewModel
#using PagedList.Mvc;
<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
#using (Html.BeginForm("Index", "Search", FormMethod.Get)) {
#Html.ValidationSummary(false)
<fieldset>
<legend>Contact Search</legend>
<div class="editor-label">
#Html.LabelFor(model => model.EmailAddress)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.EmailAddress)
#Html.ValidationMessageFor(model => model.EmailAddress)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.LastName)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.LastName)
#Html.ValidationMessageFor(model => model.LastName)
</div>
<p>
<input name="SearchButton" type="submit" value="Search" />
</p>
</fieldset>
}
#if (Model.SearchResults != null && Model.SearchResults.Count > 0) {
foreach (var result in Model.SearchResults) {
<hr />
<table width="100%">
<tr>
<td valign="top" width="*">
<div style="font-weight: bold; font-size:large;">#result.LastName, #result.FirstName</div>
#result.Title<br />
#result.Phone<br />
#result.EmailAddress
</td>
</tr>
</table>
}
<hr />
#Html.PagedListPager(Model.SearchResults,
page => Url.Action("Index", new RouteValueDictionary() {
{ "Page", page },
{ "EmailAddress", Model.EmailAddress },
{ "LastName", Model.LastName }
}),
PagedListRenderOptions.PageNumbersOnly)
}
MVC will coerce the querystring to and from your ViewModel type parameter. It's very slick!

Resources