MVC Sharing strongly typed Partial View - asp.net-mvc-4

I am trying to render a strongly typed partial view and share it between different parent views.
so I would have a parent view A that is an edit form which has element as well as partial view for Location.
#model Model.Contact
....
#Html.HiddenFor(model => model.LocationID)
#{Html.RenderPartial("../Shared/_Location", Model.Location);}
partial view is an edit form update/insert form
#model Model.Location
....with form elements bound to model passed
<div class="row-fluid">
<div class="span6">
Address 1<br />
#Html.EditorFor(model => model.Address1)<br />
</div>
<div class="span6">
Address 2<br />
#Html.EditorFor(model => model.Address2)<br />
</div>
</div>......
and view B is bounded to different model which also has a need to update Location.
#model Model.JobAssignment
....
#Html.HiddenFor(model => model.OriginLocationID)
#{Html.RenderPartial("../Shared/_Location", Model.OriginLocation);}
It's OriginLocation because this entity has multiple FKs to Location thus I named them differently in CodeFirstMapping
[ForeignKey("OriginLocationID")]
public virtual Location OriginLocation { get; set; }
[ForeignKey("DestinationLocationID")]
public virtual Location DestinationLocation { get; set; }
Database and and Entities are all fine with Fk properly setup to Location by Job and Contact.
What i had before is Location partial view would not get a location entity/model but instead be bound to whatever parent model is like #model Model.Contact just like parent view, that would work fine, as the controls in partial view is prefixed with Location.* and Location_* for ID and it works its magic, having controls as: #Html.EditorFor(model => model.Location.Address1)
but im trying to share the location partial view just like I used to do with UserControls in asp.net, so want to bind it to
I cannot get it to work, the closest I get is to bound the data, but on edit/save I get strange error.
"A referential integrity constraint violation occurred: A primary key property that is a part of referential integrity constraint cannot be changed when the dependent object is Unchanged unless it is being set to the association's principal object. The principal object must be tracked and not marked for deletion."
Please Please somebody point me to the right direction!

Related

How to dynamically switch child content in blazor pages

As I'm an absoulute beginner when it comes to web development, I started to look Blazor and learn how to use it to get an easy start in to web developlment and now struggle with a problem.
I have built a Master / Detail page and that page uses a master component (the list of employees) and 2 different detail component (employee readonly detail view and employee edit view).
The master detail page uses the following routes:
https://localhost:44344/masterdetail
https://localhost:44344/masterdetail/{id:int}
https://localhost:44344/masterdetail/{id:int}/edit
I tried to accomplish these goals:
When a user clicks a list entry from the master component, this should be shown in the URL like https://localhost:44344/masterdetail/2 and than load the employee readonly detail view into the detail area
When a user clicks the edit button located on the employee readonly detail view, the master detail page should switch to the employee edit view inside the detail area and show this in the URL like https://localhost:44344/masterdetail/2/edit
When a user clicks the save button located on the employee edit view, the master detail page should switch to the employee readonly detail view inside the detail area and show this in the URL like https://localhost:44344/masterdetail/2
The problems that I have faced:
When the user is in the readonly view and than clicks the edit button, my code is calling NavigationManager.NavigateTo($"/masterdetail/{Id}/edit"); which switches the URL in the address bar of the browser but does not invoke the OnParametersSet() lifecycle method of the master detail page.
Blazor seems to reuse the instance if the [Parameter] Id has not changed it's value.
The same happens when the user is on /masterdetail/{Id}/edit route (entered via browser address bar) and than clicks the save button.
What I learned while researching the problem:
I know that I could use the forceLoad parameter of the
NavigationManager.NavigateTo($"/masterdetail/{Id}/edit", true);
call like this, but this would lead to a complete page refresh
and I'm not sure if this is necessary.
I know that I could use EventCallback<T> in my child components and
react on these events in the parent master detail page but this seems
like a workaround.
I looked for a way to "route inside a blazor page" and stumbled
across topics like "Areas" and "Partial Views" but it looks
like these are MVC concepts.
I also found something called the "RouteView"
(https://github.com/aspnet/AspNetCore/blob/2e4274cb67c049055e321c18cc9e64562da52dcf/src/Components/Components/src/RouteView.cs)
which is a Blazor component but I had no luck using it for my
purposes.
Here is a simplified sample that shows the problem:
Create a new "Blazor App" project in Visual Studio
Choose "Blazor Server App"
Add a new .razor file and paste the code snippet in
Have a look at the comments and the code
Navigate to https://localhost:44344/masterdetail/ and try it yourself
#*Default route for this page when no entry is selected in the master list*#
#page "/masterdetail"
#*Route for this page when an entry is selected in the master list. The detail area should show a readonly view / component*#
#page "/masterdetail/{id:int}"
#*Route for this page when an entry is selected in the master list and the user clicked the edit button in the readonly view / component. The detail area should show a edit view / component*#
#page "/masterdetail/{id:int}/edit"
#using Microsoft.AspNetCore.Components
#inject NavigationManager NavigationManager
<h1>MyMasterDetailPage</h1>
<br />
<br />
<br />
<div>
<h1>Master Area</h1>
<ul class="nav flex-column">
<li class="nav-item px-3">
<button #onclick=#(mouseEventArgs => ShowListItemDetails(1))>Item 1</button>
</li>
<li class="nav-item px-3">
<button #onclick=#(mouseEventArgs => ShowListItemDetails(2))>Item 2</button>
</li>
<li class="nav-item px-3">
<button #onclick=#(mouseEventArgs => ShowListItemDetails(3))>Item 3</button>
</li>
</ul>
</div>
<br />
<br />
<br />
<div>
<h1>Detail Area</h1>
#{
if (_isInEditMode)
{
// In the real project a <EmployeeEditComponent></EmployeeEditComponent> is being used here instead of the h2
<h2>Edit view for item no. #Id</h2>
<h3>Imagine lots of editable fields here e.g. TextBoxes, DatePickers and so on...</h3>
<button #onclick=#SaveChanges> save...</button>
}
else
{
// In the real project a <EmployeeDetailComponent></EmployeeDetailComponent> is being used here instead of the h2
<h2>ReadOnly view for item no. #Id</h2>
<h3>Imagine lots of NON editable fields here. Probably only labels...</h3>
<button #onclick=#SwitchToEditMode> edit...</button>
}
}
</div>
#code {
private bool _isInEditMode;
[Parameter]
public int Id { get; set; }
protected override void OnParametersSet()
{
// This lifecycle method is not called if the [Parameter] has already been set as Blazor seems to reuse the instance if the [Parameter] Id has not changed it's value.
// For example this method is not being called when navigating from /masterdetail/1 to /masterdetail/1/edit
Console.WriteLine($"Navigation parameters have been set for URI: {NavigationManager.Uri}");
_isInEditMode = NavigationManager.Uri.EndsWith("edit");
base.OnParametersSet();
}
private void ShowListItemDetails(int id)
{
Console.WriteLine($"Showing readonly details of item no. {id}");
NavigationManager.NavigateTo($"/masterdetail/{id}");
}
private void SwitchToEditMode()
{
Console.WriteLine("Switching to edit mode...");
NavigationManager.NavigateTo($"/masterdetail/{Id}/edit");
// Setting _isInEditMode = true here would work and update the UI correctly.
// In the real project this method is part of the <EmployeeEditComponent></EmployeeEditComponent> and therefore has no access to _isInEditMode as it belongs to the <MyMasterDetailPage> component.
// I know that I could create a public EventCallback<MouseEventArgs> OnClick { get; set; } in the <EmployeeEditComponent> and react to that event here in the <MyMasterDetailPage> component but is that really the right way to do this?
//_isInEditMode = true;
}
private void SaveChanges()
{
Console.WriteLine("Saving changes made in edit mode and switching back to readonly mode...");
NavigationManager.NavigateTo($"/masterdetail/{Id}");
// Setting _isInEditMode = false here would work and update the UI correctly.
// In the real project this method is part of the <EmployeeDetailComponent></EmployeeDetailComponent> and therefore has no access to _isInEditMode as it belongs to the <MyMasterDetailPage> component
// I know that I could create a public EventCallback<MouseEventArgs> OnClick { get; set; } in the <EmployeeDetailComponent> and react to that event here in the <MyMasterDetailPage> component but is that really the right way to do this?
//_isInEditMode = false;
}
}
My setup:
Visual Studio 2019 16.3.1
.NET Core 3.0 SDK - Windows x64 Installer (v3.0.100)
What is the best practice / recommendation on how to switch child content inside a blazor page?
I asked the question on the AspNetCore Github repo and got an answer.
https://github.com/aspnet/AspNetCore/issues/16653
As "mrpmorris" said, I changed the following lines
Before #page "/masterdetail/{id:int}/edit"
After #page "/masterdetail/{id:int}/{displayMode}"
Before -
After [Parameter]<br> public string DisplayMode { get; set; }
Before _isInEditMode = NavigationManager.Uri.EndsWith("edit");
After string.Equals(DisplayMode, "edit", StringComparison.InvariantCultureIgnoreCase);
and the website behaves as intended and that solves my problem :)

SelectList dropdown list showing multiple = "multiple" for current viewmodel

This kind of a bizarre issue and I can't figure out a solution how I want.
I'm using .net core 2.1. I have a orders view model like this:
public class OrdersFilterViewModel
{
[Display(Name = "Account Numbers:")]
public IEnumerable<SelectListItem> AccountNumbers { get; set; }
}
My viewmodel and SelectList in my orders controller is called like this:
var vm = new OrdersFilterViewModel
{
AccountNumbers = new SelectList(_context.Account.Where(m => m.UserID == userId), "AccountNumber", "AccountNumber", account)
};
return PartialView("_FilterOrders", vm);
The problem lies when trying to get a dropdown list in the view which looks like this:
<form asp-action="FilterOrders" asp-controller="Order" id="ordersFilterForm" method="post">
<div class="form-group">
<label asp-for="AccountNumbers" class="control-label"></label>
<select asp-for="AccountNumbers" class="form-control" asp-items="#Model.AccountNumbers">
</select>
</div>
<div class="form-group">
<input type="submit" value="Submit" class="btn btn-default" />
</div>
</form>
This somewhat works but gives me a textarea type display where multiple = "multiple" is always tacked on in the browser. I've discovered that if I add something like the following to my viewmodel:
public int? AccountId { get; set; }
Then change my view to:
<select asp-for="AccountId" class="form-control" asp-items="#Model.AccountNumbers">
I can then have my dropdown list. However, I don't need that property for anything as far as I know. I tried a million things so it's possible I made some other slight changes I'm forgetting to get that to work, but that's the gist of it.
Is there any way around adding that extra property? Or do I need it for something I'm not aware of? Or is there any way to set multiple = "false" or something to that effect so I can get my dropdown list with my original viewmodel and such?
I haven't dealt with the post back to the controller yet, so maybe that will reveal the gotchas. I'm basically trying to create a modal type query filter that doesn't really do much other than modify some parameters and send them back to my query to update it. Thanks.
Is there any way around adding that extra property? Or do I need it
for something I'm not aware of?
Yes, you need this extra property, because in your select there are many items, and the user will select one or multiple items, and on the server side you'll need to know what the user selected, this is the purpose of the select tag.
And the multiple = "multiple" depends on what you put in the asp-for in the case of asp-for="AccountId" it is a single int value, so it won't use multiple, is you have an array in the asp-for then it will use the multiple.
Here is a pretty detailed description about the select tag helper:
Select Tag Helper in ASP.NET Core MVC

submit model with RenderPartial on View

So imagine this:
There is a View.
There is a model with this view in the following form:
Id
List<Food>
within each Food, there is:
Id
Name
List<Ingredients>
each ingredient contains:
Id
Name
Qty
This is in 1 model.
I have a view which takes in this Model.
I then have a partial view which takes in the List and renders it on the screen.
That works however when the form is submitted (the button is on the main view), the data fails to bind/is not shown in the Model.
what is the correct way to be able to bind the data back?
it does work when I take the whole thing and put it in the main view itself but due to reusability reasons, it makes sense having it in a partial view so I can just "drop" it on any page and pass the data.
The partial view takes in this:
#model List<FoodProject.Web.Models.FoodViewModel>
Thanks
UPDATE
I tried using the EditorTemplate and it seems to almost respect the model binding conventions as before using the Html.Partial was not doing so. That was producing things like:
[0].PropertyName
instead of:
ModelName[0].PropertyName
The template editor is almost there but gives me:
ModelName.[0].Id
I believe this is why when posting, I get null back in the model collection
how can I make it respect the model binding? where am I going wrong?
You should be close with the Editor Template because that worked for me. I'll show my example and maybe it will help you see what you have wrong.
The Models:
public class TestModelA
{
public List<TestModelB> PropA { get; set; }
}
public class TestModelB
{
public string PropB { get; set; }
}
The Editor Template (TestModelB.cshtml) placed in Views/Shared/EditorTemplates:
#model MvcTest.Models.TestModelB
#Html.EditorFor(m => m.PropB)
The main view:
#model MvcTest.Models.TestModelA
#using (Html.BeginForm())
{
#Html.EditorFor(m => m.PropA)
<input type="submit" value="Submit" />
}

MVC Partial view with controller, ajax - how do I ge the partial controller to get data?

I'm learning MVC and am stumped by this. I'm trying to factor some common code that gets data and displays it in a WebGrid into a partial view that i can use on multiple pages.
My home controller Index method just does a return View(). The Home view looks like this:
#using (Ajax.BeginForm("SearchAction", "Search",
new AjaxOptions { UpdateTargetId = "data-grid", HttpMethod = "Post" }))
{
#Html.TextBoxFor(model => model.name)
<input type="submit" value="Search" />
}
#{
<div id="data-grid">
#Html.Partial("SearchResults", Model)
</div>
}
I'm trying to use Ajax to avoid losing my search form data when clicking a WebGrid pager link, which are rendered as normal links.
My SearchController looks like this:
public ActionResult SearchAction(string name)
{
return RedirectToAction("SearchResults", new { name = name });
}
public ActionResult SearchResults(string name)
{
//does database query and sticks results in the viewbag
//filter on optional name parameter
VieweBag.Members = MyQueryResults;
return PartialView();
}
My SearchResults shared view, data is passed in via ViewBag.Members:
#{
var grid = new WebGrid(null, rowsPerPage: ViewBag.Pagesize);
grid.Bind(ViewBag.Members);
#grid.GetHtml(// etc. etc.)
}
The results I'm getting is that the ViewBag.Pagesize and ViewBag.Members binding fails since there is no data in the viewbag. Obviously, my partial controller is not being called to do the initial query and put stuff in the ViewBag when the home page is first loaded. How do I make that happen?
The other weird thing is that if I just copy the database query code into my home controller (where it originally was) to force the original query, then if I put some text into the search field and do a search, the partial view renders by itself on a new page. Why is that happening, I thought it would only render as part of my home page.
I've cobbled this partial view together from various answers/places and have no doubt gotten something horribly wrong :\
The partial page won't pass through a controller, but simply render the view directly. If you want to pass view data to the partial view, there is an overloaded function that takes a viewdata dictionary. I'm sorry I can't be more detailed, but I'm on my mobile (waiting for my son to fall asleep in the other room) :)
Update:
If you want to trigger a GET action for your partial view, you can use Html.Action. Here are some useful links:
MSDN RenderAction
Difference between RenderPartial and RenderAction
Further, it would probably make sense for you to move your form tags into your partial view, but those are details for when you clean up the code.
Jonass is right, the ViewBag only propagates between the Controller and the View.
One thing you can do is make the model of the partial view be the same as the type of data you're putting into the ViewBag.
So if for example your MyQueryResults is of type:
IEnumerable<Result>
In your partial view you'd add
#Model IEnumerable<Result>
And then in the main view, you'd pass it through the Render method:
#Html.Partial("SearchResults", ViewBag.Members);
You'll need to tweak this a bit to make sure it's the right type, but this should do the trick.
Good luck!

Dynamically add new fields with Editor Template

In my MVC4 project I have a Category view model with a collection of Product view models. I use an Editor Template to render a single Product view model, and pass the collection of Product view models to it:
Category view model:
#model CategoryViewModel
#using MVC4PartialViews.Models.ViewModels
<div class="editor-field">
#Html.EditorFor(model => model.CategoryName)
#Html.ValidationMessageFor(model => model.CategoryName)
</div>
#Html.EditorFor(x => x.Products)
Editor template that renders each Product in the collection:
<div class="editor-field">
#Html.EditorFor(model => model.ProductName)
#Html.ValidationMessageFor(model => model.ProductName)
</div>
// etc.
This works very well as it automagically names and indexes the elements correctly so all the Products get posted back as part of the parent Category view model - here's what it outputs:
<div class="editor-field">
<input class="text-box single-line" id="Products_0__ProductName" name="Products[0].ProductName" type="text" value="Add 1st product for this Category" />
<span class="field-validation-valid" data-valmsg-for="Products[0].ProductName" data-valmsg-replace="true"></span>
</div>
I need to lets users add and remove Products. To add a new one I somehow need to dynamically render the editor template to create the new Product and have each field indexed / named correctly i.e if I already have 2 Products (indexed 0 and 1) then the new Product would need to be named as such:
Products[2].ProductName
I've read this article by Steve Sanderson but it seems clunky and he indexes his fields with a Guid rather than a consecutive index field:
Editing a variable length list in MVC2
TIA
Not to late to answer here,
I am looking for something the link you provide useful to me. Now I know how upload file input can be add dynamically by using add/remove item in IEnumerable property of model in EditorFor.
For your solution, I think UIHint and template can do whatever you wanted to do. Inside the template you can foreach and specify the formatting for id.
Checkout the solution outlined in Jarrett Meyer's blog post, Nested Collection Models in MVC3.
Summarized in this answer on SO.