How to load partial razor page into razor view - asp.net-mvc

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.

Related

RenderAction() to make decoupled components but how to communicate with other components

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.

MVC5 How to do a post from partialview?

My partialview:
#model Alina_2017.Models.DropDownModel
<h2>Groepen</h2>
<div>
<div>
#using (Html.BeginForm("SelectGroup", "~/Controllers/WerkvormController"))
{
#Html.DropDownListFor(x => x.selectedItem, new SelectList(ViewBag.groepen, "id", "Naam"), "Select", new { #class = "form-control" })
<input type="submit" id="zoekgroep" value="Zoeken" />
}
</div>
</div>
My main view:
#model Alina_2017.Models.WerkvormModel
#{
ViewBag.Title = "Index";
}
#Html.Partial("~/Views/DropDown/Groepen.cshtml")
//More irrelevant html
My controller:
public ActionResult Index()
{
ViewBag.groep1 = convertWerkvorm(db.Werkvormens.Where(f => f.GroepenWerkvormID == 1).ToList());
ViewBag.groep2 = convertWerkvorm(db.Werkvormens.Where(f => f.GroepenWerkvormID == 2).ToList());
ViewBag.groep3 = convertWerkvorm(db.Werkvormens.Where(f => f.GroepenWerkvormID == 3).ToList());
setViewBags();
return View();
}
[HttpPost]
public ActionResult SelectGroup(DropDownModel model)
{
// the value is received in the controller.
var selectedItem = model.selectedItem;
Debug.WriteLine(selectedItem);
return View("Index");
}
I'm getting a HTTP Error 404.0 - Not Found. Is it possible to call an action from a different controller? The reason it's in a partial view is because I'm using two different models + I'll be using the partialview in multiple other views (at least once I get it to work).
Your controller's name is wrong.
Replace
#using (Html.BeginForm("SelectGroup", "~/Controllers/WerkvormController"))
with
#using (Html.BeginForm("SelectGroup", "Werkvorm"))
You can verify the actual post URL if you view your source in browser, or check network tab in the browser's development tools.
The second argument to the BeginForm() method is simply the name of the controller, not its file:
#using (Html.BeginForm("SelectGroup", "Werkvorm"))
{
}
You can post to any server-side action from anywhere. There's no limit based on how the view is rendered because once everything is rendered it's all just client-side markup no matter where it came from.
As a learning exercise, examine the actual rendered markup in your browser's debugging tools and see the URLs created for the forms. Regardless of how the partial views are arranged, which controller returned the view, what the models are, etc... It's all just HTML in the end. You can even manually write a simple .html file with a form on it which successfully posts to a server-side ASP.NET MVC action.

MVC 5 Html.ValidationSummary is not showing error

I am manually adding the following Model Error in my controller,
ModelState.AddModelError(string.Empty, "An error occurred");
However when the view is loaded, the #Html.ValidationSummary(true) is not showing any errors.
I have also tried #Html.ValidationSummary(false).
I have read many articles on this behavior and I have tried all the suggestions with no luck.
Any suggestions are greatly appreciated!
I can see it work. See the sample here: http://dotnetfiddle.net/Jz7wQj
Beware when using custom ViewData objects.
Problem Reproduced
Model
using System.ComponentModel.DataAnnotations;
namespace BrokenValidationSummaryTest.Models
{
public class MyModel
{
[Required]
public string Whatever { get; set; }
}
}
Controller
using BrokenValidationSummaryTest.Models;
using System.Web.Mvc;
namespace BrokenValidationSummaryTest.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult Submit(MyModel postedData)
{
return View(viewName: "Index", model: postedData);
}
}
}
View "Index"
#model BrokenValidationSummaryTest.Models.MyModel
<ul class="nav nav-tabs" id="carrier-tabs">
<li class="active">
<a data-toggle="tab" href="#search" class="tab-level-1" data-tab-name="search">Search</a>
</li>
</ul>
<div class="tab-content">
<div id="search" class="tab-pane fade in active">
#Html.Partial(partialViewName: "TabBody", model: Model, viewData: new ViewDataDictionary())
</div>
</div>
View "TabBody"
#model BrokenValidationSummaryTest.Models.MyModel
#Html.ValidationSummary(excludePropertyErrors: false)
<h2>TabBody</h2>
#using (Html.BeginForm(actionName: "Submit", controllerName: "Home", method: FormMethod.Post))
{
<div>Whatever:</div>
<div>#Html.TextBoxFor(x=>x.Whatever)</div>
<input type="submit" value="Submit">
}
Explanation
The partial view has the ValidationSummary HTML helper. We pass a custom ViewData object to the partial view. This custom ViewData object passed to the partial view does not contain the ViewState found in the view 'Index'. The HTML helper ValidationSummary does not have enough required information to render the error messages properly.
Recommendation: if using custom ViewData, populate the custom ViewData with the ViewState of the parent view.

How can i maintain objects between postbacks?

I'm not so experienced using MVC. I'm dealing with this situation. Everything works well until call the HttpPost method where has all its members null. I don't know why is not persisting all the data on it.
And everything works well, because I can see the data in my Html page, only when the user submit the information is when happens this.
[HttpGet]
public ActionResult DoTest()
{
Worksheet w = new Worksheet(..);
return View(w);
}
[HttpPost]
public ActionResult DoTest(Worksheet worksheet)
{
return PartialView("_Problems", worksheet);
}
This is class which I'm using.
public class Worksheet
{
public Worksheet() { }
public Worksheet(string title, List<Problem> problems)
{
this.Title = title;
this.Problems = problems;
}
public Worksheet(IEnumerable<Problem> problems, WorksheetMetadata metadata, ProblemRepositoryHistory history)
{
this.Metadata = metadata;
this.Problems = problems.ToList();
this.History = history;
}
public string Title { get; set; }
public List<Problem> Problems { get; set; } // Problem is an abstract class
public WorksheetMetadata Metadata { get; set; }
public ProblemRepositoryHistory History { get; set; }
}
And my razor view.... the razor view shows successfully my view. I realized something rare, please note in my 5 and 6 lines that I have HiddenFor method, well if I used that, when calls HTTPPOST persists the data, I don't know why.
#model Contoso.ExercisesLibrary.Core.Worksheet
<div id="problemList">
<h2>#Html.DisplayFor(model => model.Metadata.ExerciseName)</h2>
#Html.HiddenFor(model => model.Metadata.ExerciseName)
#Html.HiddenFor(model => model.Metadata.ObjectiveFullName)
#for (int i = 0; i < Model.Problems.Count; i++)
{
<div>
#Html.Partial(Contoso.ExercisesLibrary.ExerciseMap.GetProblemView(Model.Problems[i]), Model.Problems[i])
</div>
}
</div>
UPDATE
I'm using a static class to get the view name, but as I'm testing I'm just using this Partial view
#model Contoso.ExercisesLibrary.AbsoluteArithmetic.Problem1
<div>
<span style="padding:3px; font-size:18px;">#Model.Number1</span>
<span style="padding:5px; font-size:18px;">+</span>
<span style="padding:5px; font-size:18px;">#Model.Number2</span>
<span style="padding:5px; font-size:18px;">=</span>
<span style="font-size:18px">
#Html.EditorFor(model => model.Result, new { style = "width:60px; font-size:18px;" })
#Html.ValidationMessageFor(model => model.Result)
</span>
</div>
#section Scripts {
}
And here the user do the post
#model Contoso.ExercisesLibrary.Core.Worksheet
<form method="post">
#Html.Partial("_Problems", Model)
<input type="submit" value="Continue" />
</form>
The Model Binder will 'bind' or link input fields on your view to the model. It will not bind display fields (like label), that is why you need the HiddenFor it will add an <input type="hidden" which will then be bound to the Model when you Post.
You can use 'TempData'. It is used to pass data from current request to subsequent request means incase of redirection.
This link also helps you.
TempData
SO Tempdata
Make sure your form tag looks like the following, for instance the controller name, action method, the form method and an id for the form. I am referring to the #using statement. In my case the controller name is RunLogEntry, the action method is Create and the id is form.
Normal Post from View to Controller
#using (Html.BeginForm("Create", "RunLogEntry", FormMethod.Post, new { id = "form", enctype = "multipart/form-data" }))
{
<div id="main">
#Html.Partial("_RunLogEntryPartialView", Model)
</div>
}
If you want to post via Jquery, could do the following:
$.post("/RunLogEntry/LogFileConfirmation",
$("#form").serialize(),
function (data) {
//this is the success event
//do anything here you like
}, "html");
You must specify a form with correct attribute in your view to perform post action
<form action="Test/DoTest" method="post">
...
</form>
or
#using(Html.BeginForm("DoTest", "Test", FormMethod.Post)) {
...
}
The second is recommended.
Put your entire HTML code under:
#using(Html.BeginForm())
tag.

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
}

Resources