Blazo WASM - Separate into multiple components (MudBlazor) - c#

I have an edit form, with multiple MudTabPanels inside.
Problem is, I have LOTS of properties for this class, and we've decided to split into multiple panels, that each contain an edit form with different forms/inputs.
Format is somewhat like this (pseudo-razor-code) :
<MudTabs>
<MudTabPanel Text="Section 1">
<EditForm>
<MudItem>
<EditField Property1>
<EditField Property2>
...
<EditField Property 10>
</EditForm>
</MudTabPanel>
<MudTabPanel Text="Section 2">
<EditForm>
<MudItem>
<EditField Propertyn11>
<EditField Propertyn12>
...
<EditField Property 20>
</EditForm>
</MudTabPanel>
..... lots of other panels here
<MudTabPanel Text="Section N">
<EditForm>
<MudItem>
<EditField Property98>
<EditField Property99>
...
<EditField Property100>
</EditForm>
</MudTabPanel>
</MudTabs>
Problem is :
I have +1000 lines of code just in this razor page!
VS 2022 Preview is struggling to give me a decent performance (on the UI seems to be working fine)but modifying just a property is a pain in the ass in VS.
I was thinking about moving each Panel into a separate component , and transmitting my entity as a Parameter.
But:
1).Right now, because I use all these into a single page razor, on the code page, let's say I have the method DoSomething(), I can use this method on each panel.
Will I need to repeat the DoSomething() on each component, if i'll split them ? Is there a way I can share that method?
2).Do you think this will impact the performance on the UI?
3).Is there any better way of doing this ?
LE: Updated my data binding example
Code behind:
private Article _article;
Example of some bindings in my first tab:
<MudNumericField T="int?" #bind-value="_article.ArticleID">
<MudSelect #bind-Value="_article.UnitPriceIntervals" OffsetY="true" Label="Unit Price Interval" Variant="Variant.Outlined" Margin="Margin.Dense" Dense="true">
#foreach (UnitPriceIntervals? item in (UnitPriceIntervals[])Enum.GetValues(typeof(UnitPriceIntervals)))
{
<MudSelectItem Value="item">#item</MudSelectItem>
}
</MudSelect>
Now, my article properties can also contain references to other data types that are stored in a different SQL table, with possibility to change them, based on a search.
Example :
_article.GeneralText1 = 1234

Why not use a
#foreach (FieldAttributes fsa in cAtribs)
{
<MudTabPanel Text=#fsa.key>
<EditForm>
<MudItem>
#for(int i=0;i<fsa.Value.Count();i++)
{
<EditField #fsa.Value.ElementAt(i) />
}
</MudItem>
</EditForm>
</MudTabPanel>
}
loop, where Dictionary<string, List<string>> cAtribs.
The key of the dictionary is "Section 1", ... "Section N" and the collection has the property names for each section.
you can build cAtribs ahead of time, or dynamically, or even using reflection.

This is more a comment than an answer, but there's not enough spacing to fit it into a comment.
Track which Tab you're in and only load the edit form for the specific tab. That will significantly reduce what needs to be rendered at one time. Something like:
#if (tabNo = 2)
{
// Edit Form 2
}
You don't show your data binding, but make sure you use a view data service to hold your model data.
Consider using a component for each edit form within a Tab?
There are many complications with multi-tab editors/wizards. How are you validating and when? Is you backend one model/data table?
If you want more detail, add a comment and I'll try and put together some demo code later today.
===== Update
First get your data out of your edit component and into a ViewService. Here's a wire framework for one.
using System.Threading.Tasks;
namespace StackOverflow.Answers
{
public class ArticleViewService
{
//set up your data access
// load as Scoped Service - one per user session
public Article Article { get; private set; }
public Task GetArticle()
{
// Your get article code here
return Task.CompletedTask;
}
public Task SaveArticle()
{
// Your save article code here
return Task.CompletedTask;
}
}
}
Next your section edit components. Your data comes from the inject view service and gets updated directly into the same service. No passing data between componnts.
<h3>Section1</h3>
<EditForm EditContext="_editContext">
<InputText #bind-Value="ViewService.Article.Name"></InputText>
// or your mud editor components
.....
</EditForm>
#code {
[Inject] private ArticleViewService ViewService { get; set; }
private EditContext _editContext;
protected override Task OnInitializedAsync()
{
_editContext = new EditContext(ViewService.Article);
return base.OnInitializedAsync();
}
}
Then you Article editor, with MudTabs. This should track the active tab and display only the correct section component. I haven't tested this but it "should" work (I don't use MudBlazor and don't have it installed.)
<MudTabs #bind-ActivePanelIndex="this.panelId">
<MudTabPanel Text="Item One" ID='"pn_one"'>
#if(this.PanelId = 1)
{
\\ Section 1 componwnt
}
</MudTabPanel>
<MudTabPanel Text="Item Two" ID='"pn_two"'>
#if (this.PanelId = 2)
{
\\ Section 2 componwnt
}
</MudTabPanel>
<MudTabPanel Text="Item Three" ID='"pn_three"'>
#if (this.PanelId = 2)
{
\\ Section 3 componwnt
}
</MudTabPanel>
</MudTabs>
#code {
private int PanelId {
get => _panelId;
set => {
if (value != _panelId )
{
_panelId = value;
StateHasChanged();
}
}
}
private int _panelId = 1;
}

Related

.Net 6 , Blazor WASM -- paging a list of "records" for CRUD --

My project is VS-2022, .Net 6, C#, Blazor WASM hosted. For CRUD, we designed a razor page showing a list of "records" but needed a way to "page" for a list of 12 records on each page.
This post is NOT a question, but may be used by others seeking simple paging that works in similar projects. I hope this post helps others.
From this list screen, we could ADD or EDIT (delete in the future requirement). I had researched the web for how to do paging but the solutions were older and some used TagHelper which is no longer available in Blazor (or my knowledge is too weak to morph it).
So from these un-usable (in my case) solutions, I created a simple set of code to be placed on each LIST-for-CRUD-page. This process relies on threeList<> elements to hold the vehicle-data fetched from the DB once in OnParametersSetAsync() method. The paging-list is a subset of the main list of All-Vehicle-data (from the DB) and Filtered-Vehicle-data. The paging is extremely fast since paging takes place in the client-project after a single fetch of DB-data.
I show you the list page with the paging shown at the bottom of the list.
Below the image is the PagingInfo.cs class that holds the relevant paging variables. The "number of records per page" is a constructor value for this class so each CRUD-model-page can have a different number of records per page.
Followed by the pertinent HTML code that renders the paging buttons (only when there is more than a single page of 12 records).
Followed by the C#-code that drives the paging. What is NOT shown is typical Blazor code to fetch DB-data for the Vehicles and managing the Active/In-Active select control's events.
For each CRUD-model, in this case Vehicle CRUD, the C# code has a List<Vehicle> _AllVehicles variable that fetches DB-data-records for all vehicles.
A List<Vehicle> _FilteredVehicles variable that is filtered by CUSTOMER and by Active/In-Active vehicles (see the Active/In-Active select-control in the page-image).
AND a List<Vehicles> _PagedVehicles variable that contains the records to be displayed in the HTML-table (no code provided -- but see the page-image below) that is "computed/filtered" in the PageClicked() -method using Skip and Take filters for the _FilteredVehicles list.
Here is the paging-info object that is used by the page's code with information needed for the paging operations.
public class PagingInfo {
public PagingInfo(int pItemsPerPage = 8) {
ItemsPerPage = pItemsPerPage;
}
public int TotalItems { get; set; }
public int ItemsPerPage { get; set; }
public int CurrentPage { get; set; }
public int TotalPages {
get {
return (int)Math.Ceiling((decimal)TotalItems / ItemsPerPage);
}
}
}
Here is the HTML paging code below the table-body the list-"table" ( not shown here).
</tbody>
#if (_PagingInfo.TotalItems > _PagingInfo.ItemsPerPage) {
<tr class="bg-warning m-0 py-0">
<td colspan="3">
<div style="height: 3.1rem; text-align: center;" class="justify-content-center">
<div class="btn-group justify-content-center" role="group" aria-label="Basic outlined example">
<button id="btnPrev" type="button" class="btn btn-outline-dark border-3 fw-bold" #onclick="PagingClickPrevious">< Previous</button>
<span id="btnPage" type="button" class="btn btn-outline-dark disabled fw-bold">Page #_PagingInfo.CurrentPage of #_PagingInfo.TotalPages</span>
<button id="btnNext" type="button" class="btn btn-outline-dark border-3 fw-bold" #onclick="PagingClickNext">Next ></button>
</div>
</div>
</td>
</tr>
}
</table>
The paging-code follows:
// Paging Functions /////////////////////////////////////////////////////////////////////
PagingInfo _PagingInfo = new PagingInfo(12); // Initialize here: number of items-per-page.
private IEnumerable<Vehicle> _PagedVehicles { get; set; }
private void initializePagingInfo(int pPageNumber) { ////////////////////////////////////////////////////////////
_PagingInfo.CurrentPage = pPageNumber;
if (_FilteredVehicles is not null && _FilteredVehicles.Count() > 0)
_PagingInfo.TotalItems = _FilteredVehicles.Count();
}
private void PagingClicked(int pItemNumber = -1) { ////////////////////////////////////////////////////////////
_PagingInfo.TotalItems = _FilteredVehicles.Count(); // Get the count of items.
switch (pItemNumber) {
case < 1: // Goto previous page.
_PagingInfo.CurrentPage = _PagingInfo.CurrentPage < 2 ? 1 : --_PagingInfo.CurrentPage;
break;
case > 99: // Goto next page.
if (_PagingInfo.CurrentPage < _PagingInfo.TotalPages)
_PagingInfo.CurrentPage++;
break;
default:
_PagingInfo.CurrentPage = pItemNumber;
break;
}
_PagedVehicles = _FilteredVehicles.Skip((_PagingInfo.CurrentPage - 1) * _PagingInfo.ItemsPerPage).Take(_PagingInfo.ItemsPerPage).ToList();
}
private void PagingClickPrevious() { ////////////////////////////////////////////////////////////
// Event from the HTML-paging "previous" paging button -- pass -1 for previous page.
PagingClicked(-1);
}
private void PagingClickNext() { ////////////////////////////////////////////////////////////
// Event from the HTML-paging "next" paging button -- pass a large number for next page.
PagingClicked(999);
}
// end of paging functions /////////////////////////////////////////////////////////////////////////////////////

Delete all content of input text, without #bind in Blazor

I would like to know if it is possible to delete everything that the user has entered in the input text controls, using C# in Blazor, but without the controls being binded.
I have several text input but without #bind, so I cannot delete variables associated with #bind
Is it possible to do something like a "for" through the controls, and ask if it is of the input text type to delete its content?
Thanks!
So in a situation like this where you are using an extension, it's probably best to look at how the extension is implemented and work out a solution from there. If you examine the source code, you will see that the text box for the filter that you want to clear is bound to column.Filter.SearchValue:
<TextEdit Text="#column.Filter.SearchValue" TextChanged="#(async (newValue) => await OnFilterChanged(column, newValue))" />
From there, it's just a matter of being able to clear out that property in C#. Unfortunately, Blazorise does not support public access to its DisplayableColumns property -- you need that in order to manipulate SearchValue. One hacky workaround would be to subclass the DataGrid, since the property is protected:
public class MyDataGrid<TItem> : DataGrid<TItem>
{
public IEnumerable<DataGridColumn<TItem>> MyColumns => DisplayableColumns;
}
With that, it's trivial to implement a "Clear Filters" button that clears all the filters.
<input type="button" value="Clear Filters" #onclick="ClearFilters" />
Implemented as:
private void ClearFilters()
{
foreach (var column in dataGrid.MyColumns)
{
column.Filter.SearchValue = null;
}
}
You also need to capture the DataGrid in a field:
private MyDataGrid<Person> dataGrid;
For that field to be set, you need to add #ref="dataGrid" to your grid.
Now, all that said, it's pretty clear that having to create a subclass is suboptimal. I've taken the liberty of opening an issue in their GitHub repo and referenced this answer as a workaround.
The full example sans the subclass is:
<MyDataGrid TItem="Person" Data="#persons" Filterable="true" #ref="dataGrid">
<DataGridColumn TItem="Person" Field="#nameof(Person.Name)" />
</MyDataGrid>
<input type="button" value="Clear Filters" #onclick="ClearFilters" />
#code {
private MyDataGrid<Person> dataGrid;
private void ClearFilters()
{
foreach (var column in dataGrid.MyColumns)
{
column.Filter.SearchValue = null;
}
}
private Person[] persons = new[]
{
new Person { Name = "John Doe" },
new Person { Name = "Jane Down" }
};
public class Person
{
public string Name { get; set; }
}
}
Note that due to an apparent limitation in Blazor, you will need to declare MyDataGrid in a separate file, and not as a class defined in a #code block.

Rendering sequence in blazor component

I have two RenderFragments in blazor component. One is MainContent another one is AuxilaryContent i.e. I place the AuxilaryContent at first followed by MainContent. As of now, Auxilary content rendered succeeded by MainContent because as I placed AuxilaryContent at first.
But my requirement is that I need to render MainContent first, based upon rendering of MainContent, I may render AuxilaryContent or not. But in DOM, AuxilaryContent always lies before MainContent.
Is this possible?
If I am using bool in MainContent, then by using the bool to trigger SecondaryContent means, it requires another StateHasChanged(). It involves unwanted re-rendering of components.
#page "/check"
#AuxilaryContent
#MainContent
#code {
RenderFragment MainContent => (builder) =>
{
//It must be rendered first
};
RenderFragment AuxilaryContent => (builder) =>
{
//It should rendered after MainContent rendering. But in DOM, it always lies before MainContent
};
}
Suppose you have the 2 components, main and aux.
The second aux can be shown or not by the result of the main one ,thats what i understand.
First of all Blazor is a spa (single-page application) it means that everything is in fact in the same page i mean,suppose you start by the index of the page , it contains all the components of the blazor proyect.
The components inside can contain or not others.
For example, suppose a collection of books:
The index page will be more less like that:
#page "/"
<book1_component></book1_component>
<book2_component></book2_component>
<book3_component></book3_component>
Inside of each component you can put another component if u want.
<h1>book1</h1>
<label>title</label> <input type="text" />
<label>author</label> <input type="text" />
<other_component> </other_component>
#code{
//methods
}
The interesting thing about blazor is that you should work with it as you have State-Patron:
You can check here what its a state patron:
https://refactoring.guru/es/design-patterns/state
What i mean is if the state changes, then the behavior of blazor will change too.
Example: Here you can see that if the state of the index component change , there will be one or other components or even none.
#page "/"
<input type="int" max="4" min="1" #bind="state" />
#if( state == 1 )<book1_component></book1_component>
#if( state == 2 )<book2_component></book2_component>
#if( state == 3 )<book3_component></book3_component>
#if(i>4) <label> no books!</label>
#code{
int state;
}
So in your example you should do more less the same , for example, if a condition is true then you show, your component.
So if you want to refresh some component with statehaschange, you can do it in its own .razor page .
#page "/"
<InputCheckbox #bind="state" />
#if( state) <aux_component></aux_component>
else Not loading component.
#code{
bool state;
//operations to change the state
}
Other way ,can be : If you want to keep the logic on only one component, you can use the lifecycle methods:
https://learn.microsoft.com/es-es/aspnet/core/blazor/components/lifecycle?view=aspnetcore-5.0
So if you want to execute some code first put onInitiallize, then go to the next lifecycle method and execute second block by a condition, something like that.
#page "/check"
#AuxilaryContent
#MainContent
#code {
protected override void OnInitiallize(){
//executing the first
RenderFragment MainContent => (builder) =>
{
//It must be rendered first
};
}
protected override void OnParametersSet(){
//main component rendered
RenderFragment AuxilaryContent => (builder) =>
{
//It should rendered after MainContent rendering. But in DOM, it always lies before MainContent
};
}
}
Hope it helps.
I would consider restructuring this component into two nested components: main container controlling the conditional rendering of the aux one.

Loading a large list in Blazor

I would like to load a list of students at moment I have a signalr connection and the request gets all students. I'd like to scroll down the list and it would load a 100 students at a time?
<ul>
#if (Students != null)
{
#foreach (var student in Students)
{
<li>
<div class="treeview__item__header">
#plan.Name
</div>
</li>
}
}
</ul>
#code
{
private List<StudentsData> Students { get; set; }
protected override async Task OnInitializedAsync()
{
Students = await StudentsConnection.GetStudents();
}
}
I have used this example with success:
https://github.com/amuste/DnetVirtualScrolling
in alternative, as Alexander stated, a simple pager calculated on the number of rows to show and seems to be the best "pure blazor" solution
Edit: Looks like Virtualize could be perfect for this use:
https://www.syncfusion.com/blogs/post/asp-net-core-blazor-component-virtualization-in-net-5.aspx
I believe that you should consider solution on store procedure level with
Select top 100 where and have a parameter #next
With witch you could control from app with button or event when you scroll down to the end of list
you can use pagination and implement a pager component. Look at these two articles, they may help you: Pager component and part of Pager component 2

Strange behaviour in ASP.NET MVC: removing item from a list in a nested structure always removes the last item

Scenario
I have a parent/child model (to be exact a small questionnaire form and a one or more number of contacts). For historic reasons, all of this would have been done on the same form so user would have a form for the parent and one child and they would hit a button to add more children. Child has a few standard fields and the same with the parent, nothing fancy. Main requirement is that the data must not touch the database until all is valid and setup while I would have to go back to server for adding deleting children.
Implementation
It was very quick to get this working in ASP.NET MVC (using MVC 2 with VS 2010). I got two models, one for parent and one for the child and got only one controller. Controller has a Create Method which is a get and gets a default view with a fresh brand new parent containing one child. I use editor template for the child model which works nicely.
I have one HTML form which has a "save" and "add child" and I have "delete" button for each form. Since this cannot be stored in database, I store the temp model in the form itself and it goes back and forth between browser and server. Perfromance is not much of an issue here but the cost of development since there are quite a few of these forms - so please do not get distracted too much by suggesting an alternative approach although I appreciate comments anyway.
In order to find out which child to delete, I create temp GUID Ids and associate them with the child. This will go onto the HTML input's value for delete button (usual trick when you have multiple actions and the same form).
I have disabled caching.
Issue
Please have a look at the snippets below. I have debugged the code and I have seen always correct GUID being passed, correct item removed from the list in the controller and correct items being rendered in the template. BUT ALWAYS THE LAST ONE GETS DELETED!! I usually click the first delete and can see that the last gets deleted. I carry on and first item is the last being deleted.
Controller
public ActionResult Create()
{
EntryForm1 entryForm1 = new EntryForm1();
entryForm1.Children.Add(new Child("FILL ME", "FILL ME"){ TempId = Guid.NewGuid()});
return View("EntryForm1View", entryForm1);
}
[HttpPost]
public ActionResult Create(EntryForm1 form1, FormCollection collection, string add)
{
if (add == "add")
form1.Children.Add(new Child("FILL ME", "FILL ME") {TempId = Guid.NewGuid()});
var deletes = collection.AllKeys.Where(s => s.StartsWith("delete_"));
collection.Clear();
if (deletes.Count() > 0)
{
string delete = deletes.FirstOrDefault();
delete = delete.Replace("delete_", "");
Guid g = Guid.Parse(delete);
var Children = form1.Children.Where(x => x.TempId == g).ToArray();
foreach (Child child in Children)
{
form1.Children.Remove(child);
}
// HERE CORRECT ITEM IS DELETED, BELIEVE ME!!
}
if (ModelState.IsValid)
{
return Redirect("/");
}
return View("EntryForm1View", form1);
}
View snippet
<% for (int i = 0; i < Model.Children.Count;i++ )
{%>
<h4> <%: Html.EditorFor(m=>m.Children[i])%></h4>
<%
}%>
<p>
<input type="submit" value="Create" name="add" />
<input type="submit" value="add" name="add" />
</p>
Child Editor template snippet
<%: Html.HiddenFor(x=>x.TempId) %>
</span>
<input type="submit" name='delete_<%: Html.DisplayTextFor(m => m.TempId) %>' value="Delete" />
Many thanks for your time and attention
UPDATE
I was asked for model classes and I am sharing them as exactly as they are.
Entryform1 is the parent and Somesing is the child.
public class Somesing
{
public Somesing()
{
}
public Somesing(string o, string a) : this()
{
OneSing = o;
AnozerSing = a;
}
[StringLength(2)]
public string OneSing { get; set; }
[StringLength(2)]
public string AnozerSing { get; set; }
public Guid TempId { get; set; }
}
public class EntryForm1
{
public EntryForm1()
{
Sings = new List<Somesing>();
}
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public List<Somesing> Sings { get; set; }
}
I believe that problem lies with ModelState. When the view gets rendered, which I assume is where the issue lies, after the POST, the last value is not displayed i.e. removed from the view.
The issue is that Model.Children.Count will return the correct number of elements to display.
Lets break this down...
So if you have initially had 5 then removed the first one which is at index 0 based on the Guid, you now have items 4 items left with indexes 1 to 4.
However, when rendering the view after the post, the HtmlHelpers do not look at the values in model posted, but rather the values contained within the ModelState. So in the ModelState, item with index 0 still exists and since the loop is now looping to 4, the last element will not be displayed.
The solution, use ModelState.Clear()
OK, as Ahmad pointed out, ModelState is the key to the issue. It contains the collection as such:
FirstName
LastName
...
Sings[0].OneSing
Sings[0].AnozerSing
Sings[1].OneSing
Sings[1].AnozerSing
Sings[2].OneSing
Sings[2].AnozerSing
Now if I delete item 0 from the list, now the items will move up in the list and the data in the ModelState will go out of sync with the model. I had expected ASP.NET MVC to be clever enough to find out and re-order, but well that is asking for too much.
I actually implemented PRG (post-redirect-get) and by keeping the model in session, I was able to display correct information but again, this will remove all the validation in the collection and if model itself is valid, it will happily save and redirect back to home "/". Clearly this is not acceptable.
So one solution is to remove all items in the ModelState and then add a new entry for the model itself (with key of EmptyString). This can actually work alright if you populate it with error "Item deleted" as this will be displayed in the validation summary.
Another solution is to manually change the items in the model state and re-arrange them based on the new indexes. This is not easy but possible.
ModelState.Clear() will Solved this problem.
ModelState.Clear() is used to clear errors but it is also used to force the MVC engine to rebuild the model to be passed to your View.

Categories

Resources