Use forms on multiple tabs in Net Core 3.1 MVC - tabs

In my Net Core 3.1 MVC project I wish to create two tabs on one page, with on each tab a form for editing data. What is the best approach to do this? Preferably with lazy loading, so that form data gets loaded when a tab is active.
I have tried with Pages and PartialViews but this didn't work properly, and now I am wondering if there is a straightforward way of achieving this in Net Core MVC.
My code so far:
basicprofile.cshtml
#page
#model VideoGallery.Presentation.Pages.Account.BasicUserProfile.InputModel
#{
}
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="basicprofile-tab" data-toggle="tab" href="#basicprofile" aria-controls="basic" aria-selected="true">Account</a>
</li>
<li class="nav-item">
<a class="nav-link" id="extendedprofile-tab" data-toggle="tab" href="#extendedprofile" aria-controls="extended" aria-selected="false">Profile</a>
</li>
</ul>
<div class="tab-content p-3 border-right border-left">
<div class="tab-pane fade show active" id="basicprofile" role="tabpanel" aria-labelledby="basicprofile-tab">
</div>
<div class="tab-pane fade" id="extendedprofile" role="tabpanel" aria-labelledby="extendedprofile-tab"></div>
</div>
#section scripts{
<script>
var basicprofileLoaded = false;
var extendedprofileLoaded = false;
console.log("active");
$(function () {
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
switch ($(e.target).attr('aria-controls')) {
case "basic":
if (!basicprofileLoaded) {
$('#basicprofile').load("#Url.Page("basicuserprofile", pageHandler:"PartialForm" )");
basicprofileLoaded = true;
}
break;
case "extended":
if (!extendedprofileLoaded) {
console.log("inside switch/ IF extended part");
$('#extendedprofile').load('/account/extendedprofile')
ordersLoaded = true;
}
break;
}
});
});
</script>
}
BasicUserProfile.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using VideoGallery.Presentation.Services;
namespace VideoGallery.Presentation.Pages.Account
{
public class BasicUserProfile : PageModel
{
[BindProperty]
public InputModel Input { get; set; }
public class InputModel
{
public string LoginName { get; set; }
public string Password { get; set; }
}
// both methods don't even get called, even when the page renders with an empty form.
public async Task<IActionResult> OnGetAsync()
{
Input.Password = "test inpu";
Input.LoginName = "other input";
return Page();
}
public async Task<PartialViewResult> OnGetPartialForm()
{
Input.LoginName = "some name";
return Partial("_BasicProfilePartial", Input);
}
}
}
I have several problems:
My handlers OnGet() and OnGetPartialForm() are not fired. Break points don't get hit.
On loading of BasicUserProfile page, the basic tab should be active, showing the form on this first tab. It has the active class, but the data isn't loaded.
The BasicProfile tab loads the whole Page object, after I click the basic tab (when I am on the extended-tab, for example). It loads the whole page, although none of the backend breakpoints are hit.
What I would like to do:
On loading the initial page, the form data for the first tab (active on loading) should be loaded, with or without data. Then if the user changes to the second tab, that form data should be loaded and displayed under the 2nd tab. Obviously, later, I want to process the data with a post request.

My handlers OnGet() and OnGetPartialForm() are not fired. Break points don't get hit.
That is because you use #model VideoGallery.Presentation.Pages.Account.BasicUserProfile.InputModel , which is not a PageModel.
Change to:
#page
#model VideoGallery.Presentation.Pages.Account.BasicUserProfile
<input asp-for="Input.Password" />
<input asp-for="Input.LoginName" />
$('#basicprofile').load("#Url.Page("basicuserprofile", pageHandler:"PartialForm" )");
Be sure the url is rendered successfully. You can press F12 and click Sources panel to check the js source in browser. In your scenario, it seems to be:$('#basicprofile').load("#Url.Page("/Account/basicuserprofile", pageHandler:"PartialForm" )");.
[BindProperty]
public InputModel Input { get; set; }
You need remember to initialize the InputModel, otherwise it will get error when hit Input.Password = "test inpu";:
[BindProperty]
public InputModel Input { get; set; } = new InputModel();

Related

Custom Blazor Server Component Not Invoking the OnClick Function

I have created a layout component in the same directory as with the MainLayout component. The CustomLayout #inherits LayoutComponentBase and has #Body which gets the content to be rendered inside the layout.
As obviously expected, the layout component has its own css files and UI display is fine. I am only having a problem with the #onclick function. It cannot be invoked. Is there a way to trigger the function on button click?
The reason I want to do this is that I am creating a navigation bar which will have to show/hide a dropdown. I hope I can get some ideas on this. Appreciated.
I am calling a server function this way: #onclick="SomeFuction" or #onclick="SomeFuction" or #onclick="#(() => SomeFuction())"
LayoutComponent:
#inherits LayoutComponentBase
#Body
Home page referencing a custom layout:
[AllowAnonymous]
[Layout(typeof(CustomLayoutComponent))]
[AllowAnonymous] is just for allowing anonymous access.
I am using Blazor Server and .NET 6
Additional Information:
I have put all the code in one page so that it becomes easier to read and understand.
Here is the LayoutComponent razor component:
#inherits LayoutComponentBase
<div class="navbar" id="custom-navbar">
<div class="wrapper">
<a>Home</a>
<a>Service</a>
<button class="btn btn-primary" #onclick="ToggleProductsUI">Products</button>
</div> </div>
<!--Component to show Products UI Component. Scoped css for styling-->
<ProductsComponent TValue="string" UIState="#ProductsUIState"></ProductsComponent>
#code{
// state of the Products submenu Component
string ProductsUIState { get; set; }
// a control function for toggling the Products UI
// this function somehow is not invoked.
void ToggleProductsUI()
{
if (string.IsNullOrWhiteSpace(ProductsUIState))
{
ProductsUIState = "show-ui";
return;
}
ProductsUIState = string.Empty;
}
}
Here is the ProductsUI component code (I have removed the unnecessary event to avoid confusion. The component can be fully controlled by the parent):
public partial class ProductsComponent<TValue> {
[Parameter]
public string? UIState { get; set; } }
Blazor pages that will use the custom layout component will point to it like this:
[AllowAnonymous]
[Layout(typeof(CustomLayoutComponent))]
public partial class Index
{
}
This is working fine. The issue is in the CustomLayoutComponent when trying to invoke the ToggleProductsUI() function
I accidentally inluded a <body> element in the layout and as such, the <script src="_framework/blazor.server.js"></script> was not being run hence the click event was not being invoked. I removed the <body> element. I was misled by the default body{} style in the underlining scopped css. Resolved.

How to add attribute on each blazor component automatically by rule?

For testing purpouses I want to have custom attribute on each, for example, button each page of my app without any manual adding.
Let's say I have page:
#page "/mypage"
<h1>This is page</h1>
<button class="btn btn-primary" #onclick="IncrementCount">Click me</button>
<button class="btn btn-primary" #onclick="IncrementCount">NO, click ME</button>
#code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
and, eventually, after some kind of additional rendering step, I want to see this html in my browser:
<h1>This is page</h1>
<button class="btn btn-primary" my-custom-attribute="button-click-me">Click me</button>
<button class="btn btn-primary" my-custom-attribute="button-no-click-me">NO, click ME</button>
So, the rule for makeing my-custom-attribute content comes from component content itself. It can be described as "component name + dash + text from component to lower case" => button(component name) + - + click-me(text from component to lower case with dashes instead spaces). Rule should be described somewhere in C# code and should "targeting" on list of components(buttons, divs, etc).
Is there any way to achieve that? Maybe there is a way to tweak Blazor rendering process somehow?
I can see no other solution than using a custom generic component. Someone may come up with a better answer. If so, I'll happily delete this one.
Here's a basic custom component to render html elements:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace BlazorApp1.Pages
{
public class MyComponentBase : ComponentBase
{
[Parameter] [EditorRequired] public string Tag { get; set; } = "div";
[Parameter] public string MyAttributeValue { get; set; } = string.Empty;
[Parameter] public string? Content { get; set; }
[Parameter] public RenderFragment? ChildContent { get; set; }
[Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object> UserAttributes { get; set; } = new Dictionary<string, object>();
private string myAttribute => string.IsNullOrWhiteSpace(Content) ? this.MyAttributeValue : this.Content.Replace(" ", "-");
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, Tag);
builder.AddMultipleAttributes(1, UserAttributes);
if (!string.IsNullOrWhiteSpace(this.myAttribute))
builder.AddAttribute(2, "my-attribute", this.myAttribute);
if (ChildContent is not null)
builder.AddContent(3, ChildContent);
else if (!string.IsNullOrWhiteSpace(Content))
builder.AddMarkupContent(4, Content);
builder.CloseElement();
}
}
}
And a Demo:
#page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<MyComponentBase Tag="button" MyAttribute="Blazor-Component" class="btn btn-primary" #onclick=this.OnClick Content="My Blazor Button" />
<MyComponentBase Tag="div" MyAttributeValue="Survey-Prompt-Div" class="m-2">
<SurveyPrompt Title="How is Blazor working for you?" />
</MyComponentBase>
<div>
#this.message
</div>
#code {
private string message = string.Empty;
private void OnClick()
{
this.message = $"Hello Blazor at {DateTime.Now.ToLongTimeString()} ";
}
}
While this sort of works, it highlights some issues with what you are trying to do.
You can't just get the "content" between the opening and closing tags of an element. The content is a RenderFragment, not a string that you can parse and use for your attribute. Therefore anything like the second block in the example requires a manual setting of the attribute (you may as well define it directly!).
The complexity of the component depends on what type of content you want to apply your attribute to.
Anyway, the component demonstrates the basic principles of how to capture parameters and attributes and use them to construct elements.

How to capture data in a DropDownListFor in a form and pass it to the routeValues

I know this is fairly simple, and asked often, but wiring it all together is still new to me.
Each Invoice has an Id:
Model
public class Invoice
{
public int Id { get; set; }
}
I use the viewModel to populate a list of selections for a dropdown, which works great. After returning to a new view, Main, the viewModel is also used to capture the InvoiceId of the selection from the dropdown.
viewModel
public class MainPageViewModel
{
public int InvoiceId { get; set; }
public IEnumerable<Invoice> Invoices { get; set; }
}
Controller
public ActionResult Index()
{
var userId = User.Identity.GetUserId();
var viewModel = new MainPageViewModel
{
Invoices = _context.Invoices.Where(i => i.User.Id == userId).ToList();
};
return View("Main", viewModel);
}
And I want to use razor: #Html.BeginForm() to route to the {controller}/{action}/{id} URL using this cshtml:
View
#model SubSheets.ViewModels.MainPageViewModel
#using (Html.BeginForm(
action: "WorkSummary",
controller: "Invoices",
routeValues: new { id = Model.InvoiceId }))
{
#Html.AntiForgeryToken()
<div class="form-group">
<div class="col-md-10">
#Html.DropDownListFor(
expression: m => m.InvoiceId,
new SelectList(
Model.Invoices,
dataValueField: "Id",
dataTextField: "Name"
)
)
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Go To Invoice" class="btn btn-
default" />
</div>
</div>
}
And then simply use convention in the new view, which is building the URL correctly. The problem is the id == 0 no matter which selection is selected.
public ActionResult WorkSummary()
{
return View();
}
As I understand it, the lambda expression in the DropDownListFor determines the data hook in the viewModel object and the dataValueField of the SelectList captures the selection's Id in this case and assigns it to the expression.
I assumed it would complete the assignment and pass the Id to the viewModel and pass that through to the routeValues after submitting the form, however it passes 0 instead of the correct Id.
If I load the new view with the viewModel, I can render the id correctly using razor on the page. E.g.:
#using SubSheets.ViewModels.MainPageViewModel
<p>#Model.InvoiceId.ToString()</p>
What am I missing here?
Edit:
I've tried moving the dropdown outside of the form and making a separate submit button:
<button class="btn btn-default">Button</button>
I feel like I'm trying to force a square peg into a circular opening. Am I going about this incorrectly?
Edit2
Well, I came up with a different solution. Realizing I want to use the Url helper, I used Bootstrap to make each selection clickable which is way sleeker anyway.
<div class="btn-group">
<button
type="button"
class="btn btn-default dropdown-toggle"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
Select Invoice <span class="caret"></span>
</button>
<ul class="dropdown-menu">
#foreach (var invoice in Model.Invoices)
{
<li><a href="#Url.Action(
"WorkSummary",
"Invoices",
new {id = invoice.Id},
null)">
#invoice.Name
</a></li>
}
</ul>
</div>
then pass the id into the controller, query the database and return the correct entry with the correct URL. It's Ok to spend half a day on something that took me 5 minutes to actually build, right?

Multibutton form doesn't map data back to textboxes .net core 1.1

Ok, so now I'm trying to learn .net core mcv and I'm having a problem mapping data from MySQL back to my form. When I make the form as a single text box with a single button, and the other fields outside the form (but on the same page), the mapping works fine. If I include all the fields within the form, the data is obtained but not displayed on the page. I have even gone so far as to code one of the multiple submit buttons as an update of the data. I use the first text box to get the item from the database, which it does (but does not map to the text-boxes), then in the second text box (which should have the existing data, but is empty) I put the information to update in the database, click on the submit button for that text box, and the database is updated (but the text boxes in the view remain blank).
My model:
using System;
namespace DbTest.Models
{
public class ProductInventory
{
public string Field1 { get; set; }
public string Field2 { get; set; }
public string Field3 { get; set; }
public int Field4 { get; set; }
}
}
my controller:
using System;
using Microsoft.AspNetCore.Mvc;
using MySql.Data.MySqlClient;
using Microsoft.AspNetCore.Authorization;
using DbTest.Models;
namespace DbTest.Controllers
{
public class InventoryController : Controller
{
// [Authorize]
public IActionResult Index()
{
return View();
}
[HttpPost]
public IActionResult ProcessForm(string button, ProductInventory p)
{
IActionResult toDo = null;
if (button == "Button1")
{
toDo = GetItem(p);
}
if (button == "Button2")
{
toDo = UpdateField2(p);
}
if (button == "Button3")
{
toDo = UpdateField3(p);
}
if (button == "Button4")
{
toDo = UpdateField4(p);
}
return toDo;
}
// [HttpPost]
public IActionResult GetItem(ProductInventory p)
{
//CODE SNIP - DATABASE QUERY, IT ALL WORKS, SO WHY BOTHER YOU WITH THE DETAILS?
return View("Index", p);
}
public IActionResult UpdateField2(ProductInventory p)
{
//CODE SNIP - DATABASE UPDATE, ALL WORKS, NOTHING TO SEE HERE
return View("Index", p);
}
}
}
And finally, my view:
#model DbTest.Models.ProductInventory
#{
ViewData["Title"] = "Inventory Page";
}
#using (Html.BeginForm("ProcessForm", "Inventory", FormMethod.Post))
{
<div>
Search Item (Field 1):
#Html.TextBoxFor(model => model.Field1)
<input type="submit" name="button" value="Button1" />
</div>
<div>
Field 2:
#Html.TextBoxFor(model => model.Field2)
<input type="submit" name="button" value="Button2" />
</div>
<div>
Field 3:
#Html.TextBoxFor(model => model.Field3)
<input type="submit" name="button" value="Button3" />
</div>
<div>
Field 4:
#Html.TextBoxFor(model => model.Field4)
<input type="submit" name="button" value="Button4" />
</div>
}
To reiterate, if I close the form after Button1:
#using (Html.BeginForm("ProcessForm", "Inventory", FormMethod.Post))
{
<div>
Search Item (Field 1):
#Html.TextBoxFor(model => model.Field1)
<input type="submit" name="button" value="Button1" />
</div>
}
<div>
Field 2:
//etc.
the mapping works, but only the first field and button of the form work. With the form around all four fields and buttons, the mapping doesn't work, but the coding of the second button DOES update the database on clicking Button2.
Can someone explain what I've done wrong here?
Thanks!
At first, don't use html helpers in ASP.NET Core.They work but it is not best practice. Instead use tag helpers wherever possible. Furthermore, don't use your db models as view models.
Regarding your Index action: You forgot to pass a view model to your view.
In your ProcessForm action you instantiate IActionResult and then assign it with a (action) function. Don't do that. Instead use return RedirectToAction("ActionName");.
In your case I would handle the DB updates inside the ProcessForm action or in a function, which doesn't return IActionResult.
In conclusion, I can only recommend you to read the ASP.NET Core documentation and then ask again if you still don't get it to work. I recommend you to start with reading this.

Retain jquery tabs after redirect

I am having jquery tabs inside the parent menus, I want to retain the opened tabs after the page redirect.
HttpContext.Current.Response.Redirect("~/HRMS/PayrollHome.aspx", false);
You could always store when tabs are opened and closed in the session. Then when the page is visited again you can check if each tab is listed as being open in the session and if it is re-open it.
EDIT
Its difficult to give you entire code snippets that answer your problem completely because I haven't got access to your code base. But it would be something like this.
<ul id="tabs">
<li onclick="$.get("SaveTab.ashx?tab=1");">Tab 1</li>
<li onclick="$.get("SaveTab.ashx?tab=2");">Tab 2</li>
</ul>
Then in the SaveSession handler
<%# WebHandler Language="C#" Class="SaveTab" %>
using System;
using System.Web;
public class SaveTab: IHttpHandler, System.Web.SessionState.IRequiresSessionState
{
public void ProcessRequest (HttpContext context) {
context.Response.ContentType = "text/plain";
if (HttpContext.Current.Request.QueryString["tab"] != null)
{
HttpContext.Current.Session["OpenTab"] = HttpContext.Current.Request.QueryString["tab"];
}
}
public bool IsReusable {
get {
return false;
}
}
}
Then in the ASPX output some Javascript that selects the tab. Notice that I am outputting the session value.
$(document).ready(function() {
$('#tabs').tabs('select', <%= SESSION["OpenTab"]%>);
});
I hope this is enough for you to get the general idea, I can't obviously tell you exactly what to do just show you a good approach.