Is it possible to cache the output of a RenderFragment in Blazor WebAssembly?
Specifically, this is to retain components shown intermittently without rendering them to the browser. With "rendering to the browser" here I mean outputting HTML to the browser.
I am trying to get this to work to improve performance in a library I am writing where two-dimensional data is shown in the browser. The resulting grid is virtualized to prevent having tens of thousands of elements in the DOM since having that many elements results in a degraded experience.
When virtualizing, elements outside the view are not rendered only to be rendered when they shift into view.
The caching mechanism should preserve the HTML output of Razor Components such that the components can be removed from and added to the DOM without having to be reinitialized and rerendered.
Currently, I have not found a way to achieve this.
A basic set-up to reproduce what I have tried so far is as follows:
create a Blazor WebAssembly project using the default template without hosting.
Add a Razor Component with the name ConsoleWriter.razor to Shared and set the contents as follows:
<h3>ConsoleWriter #Name</h3>
#code {
[Parameter]
public string? Name { get; set; }
protected override void OnInitialized() {
Console.WriteLine($"ConsoleWriter {this.Name} initialized");
}
protected override void OnAfterRender(bool firstRender) {
if(firstRender) {
Console.WriteLine($"ConsoleWriter {this.Name} rendered");
} else {
Console.WriteLine($"ConsoleWriter {this.Name} re-rendered");
}
}
// Trying to stop rerendering with ShouldRender.
// Does not stop the rendering in scenario's where the component has just been initialized.
protected override bool ShouldRender() => false;
}
Replace the contents of Index.razor with the following code:
#page "/"
<p>Components are showing: #showComponents</p>
<button #onclick="() => this.showComponents=!this.showComponents">Toggle</button>
#* Here the components are in the DOM but can be hidden from view, still bogging down the DOM if there are too many. *#
<div style="#(this.showComponents ? null : "display:none;")">
<ConsoleWriter Name="OnlyHidden" /> #* Does not intialize or rerender when showComponents is toggled *#
</div>
#* Here the components are not in the DOM at all when hidden, which is the intended scenario but this initializes the components every time they are shown. *#
#if(this.showComponents) {
<ConsoleWriter Name="Default" /> #* Intializes and rerenders when showComponents is toggled *#
#consoleWriter #* Intializes and rerenders when showComponents is toggled *#
<ConsoleWriter #key="consoleWriterKey" Name="WithKey" /> #* Intializes and rerenders when showComponents is toggled *#
}
#code {
private bool showComponents = false;
private object consoleWriterKey = new object();
private RenderFragment consoleWriter { get; } = builder => {
builder.OpenComponent<ConsoleWriter>(0);
builder.AddAttribute(1, nameof(ConsoleWriter.Name), "RenderFragment");
builder.CloseComponent();
};
}
When running the project and checking the browser console, you can see which components report being reinitialized or rerendered.
The components can be toggled by clicking the button.
Unfortunately, all of those being removed from and added to the DOM report back whenever being toggled to be shown despite their content never changing.
Does anyone have another idea how to approach this?
Related
I have a razor component which displays a menu:
<div class="menu">
Home
Report
<AuthorizedView Roles="admin">
Admin Page
</AuthorizedView>
</div>
In the above example, the main(home) page is active by default. Once the user clicks on any of the items, the active class needs to be removed from any previous item and assigned to the newly clicked item.
What is the best way to do that in a blazor server app?
Here is the current work around I am using:
<div class="menu">
<a href="/"
class="item #(IsHomeActive?"active":"")"
#onclick="()=> { ClearActive(); IsHomeActive=true; }"> Home </a>
<a href="/report"
class="item #(IsReportActive?"active":"")"
#onclick="() => { ClearActive(); IsReportActive = true;} ">Report</a>
<AuthorizedView Roles="admin">
<a href="/admin"
class="item #(IsAdminActive?"active":"")"
#onclick="() => { ClearActive(); IsAdminActive = true;}">Admin Page</a>
</AuthorizedView>
</div>
#code {
bool IsHomeActive = true;
bool IsReportActive = false;
bool IsAdminActive = false;
private void ClearActive() {
IsHomeActive = false;
IsReportActive = false;
IsAdminActive = false;
}
}
This solution works fine, but it doesn't scale well. If I need to add more items to the menu, I will have to remember to added a boolean for the new item and remember to include it in ClearActive().
I though about creating a special data structure (an array, or list) and then have the code iterate through all items, this way I will only need to add one item to the array and then the code will generate the required html for the element, and also will handle the click event and automatically clear all active classes from the array and assign it to the clicked item in the array. But that approach will sacrifice the ability to use <AuthorizedView> on special items in the array..
I have a Blazor Server app. I dynamically add a custom control (which I created in a Razor Class Library) to my page, like this:
<div class="container ">
<div class="row ">
<div class="col-12">
<label>#p_section.question</label>
</div>
</div>
#foreach (object new_control_model in p_section.list_of_control_models)
{
DisaggregateControlModel new_disag_model = (DisaggregateControlModel)new_control_model;
<DisaggregateControl #ref="myComponents[new_disag_model.id]" model="new_disag_model">
</DisaggregateControl>
}
</div>
This add the control to my dictionary, which I can access.
#code {
private Dictionary<string, object> myComponents = new Dictionary<string, object>();
}
In the custom control, I have a method that sets a bool, which allows me to display or hide a Div. In the web component that has this code, I want to iterate over all the objects in myComponents and either turn on or off the div display. I do that like this:
foreach (string id in some_list_of_ids){
//find the object in myComponents list
object found_obj = myComponents.FirstOrDefault(x => x.Key == id).Value;
//cast to my custom control
DisaggregateControl myControl = (DisaggregateControl)found_obj;
// based on property, determine if I should show the div or not
if(myControl.some_vale >0){
//show
myControl.showDiv(true);
}else{
myControl.showDiv(false);
}
}
//updated all the controls, so update the page
StateHasChanged();
If I debug and walk through, I can see that the code works. The correct divs are shown/hidden. Until the code reaches StateHasChanged(), and then all the divs are hidden. If I remove StateHasChanged then the code also does not work (the divs are not shown, when they should be).
I am not sure what the issue is or how to best handle this?
Turns out that it was an issue with my variable. In the custom library, I showed/hid the div by setting a bool value. But, I inadvertently made the bool static. Once I changed that, it worked fine. I assume because the variable was static, once I changed it for one control, it got changed for all controls.
I am trying to make my input text always be uppercase in a blazor input text field. I tried creating a custom InputText like the below but it doesn't update the binding (appears as uppercase but doesn't bind as such). What am I doing wrong here? Is there a better way?
#inherits InputText
<input #attributes="AdditionalAttributes"
class="#CssClass"
value="#CurrentValue"
#oninput="EventCallback.Factory.CreateBinder<string>(
this, __value => CurrentValueAsString = __value.ToUpper(), CurrentValueAsString.ToUpper())" />
Simplicity works for me.
<input type="text" oninput="this.value=this.value.toUpperCase()" #bind=CurrentValueAsString />
For simplicity and (in my opinion) UX reasons, let's assume that the user is allowed to type lowercase letters, but the application will ToUpper the input after they leave that field.
First, make a new component so we can reuse the logic. I called mine UppercaseTextInput.razor
<input value="#UserInput"
#onchange="async e => await OnChange(e)" />
#code {
[Parameter]
public string UserInput { get; set; }
[Parameter]
public EventCallback<string> UserInputChanged { get; set; }
private async Task OnChange(ChangeEventArgs e)
{
var upperCase = (e.Value as string).ToUpperInvariant();
await UserInputChanged.InvokeAsync(upperCase);
}
}
The private method OnChange gets called whenever the input loses focus. Change #onchange to #oninput to make this happen every keystroke.
Now you can use this component in another page or component. For example, in the index page:
#page "/"
<h1>Hello, world!</h1>
<UppercaseTextInput #bind-UserInput="UserInput" />
<p>You typed: #UserInput</p>
#code {
public string UserInput { get; set; } = "Initial";
}
In this example, I have "Initial" as the starting text, which will be printed inside the text box. As soon as you leave the element, the text inside will be transformed to be uppercase.
The benefit of using a component is that you can do a standard #bind- with your properties.
I was using an old reference to the input which was causing the error. There was a bug in the code though (if the input was null). The corrected version can be seen here:
#inherits InputText
<input #attributes="AdditionalAttributes"
class="#CssClass"
value="#CurrentValue"
#oninput="EventCallback.Factory.CreateBinder<string>(
this, __value => CurrentValueAsString = __value?.ToUpper(), CurrentValueAsString?.ToUpper())" />
The answer to this was easier than all the answers: it can be solved using CSS text-transform: uppercase;. This is all you needed and it doesn't take any processing power because it's included in the browser's engine.
I'm using MudBlazor.
I scratched my head for a while why the heck adding
Style="text-transform: uppercase;"
to <MudInput> is not working, even with !important attribute the default user agent style (text-transform: none;) was applied.
C# or JS seemed overkill for the task in my opinion.
What finally worked for me was adding
input {
text-transform: uppercase;
}
to the site's CSS sheet (index.css in my case).
This eventually overwritten the default user agent style.
How can I make a simple "jump to" part of already loaded page in Blazor? Like this in HTML:
Contact us
...
<section id="contact">
Ideally I also want to have it smooth scroll down to this section. Thought I would try to solve this with CSS, but maybe not possible?
I've solved this by using a button and then writing some inline Javascript in the markup. You can generalize this to a Blazor component for bonus points!
<button type="button" onclick="document.getElementById('contact').scrollIntoView({behavior:'smooth'})">Contact us</button>
...
<section id="contact">
What you need is the hashed routes features of Blazor. But, alas, no such features do exist yet. I'd suggest you use JSIterop to perform this task: Create a JavaScript that performs the navigation, and pass it an ElementRef object.
Hope this helps...
Edit: The following is an adaptation of the best workaround solution I've found in Github...
Ordinarily, when you click the link to contact, you get redirected to the route http://localhost:5000/mypage#contact, but will be at the top of the page. The fragment of the route is not used for selection of a specific HTML element.
The current workaround is to write explicit code that interprets the URL. In the example above, we could use a little bit of JavaScript and then call that from our Blazor code:
mypage.cshtml:
#page "/mypage"
#inject Microsoft.AspNetCore.Components.Services.IUriHelper UriHelper
<nav>
contact
</nav>
<section>
<h2 id="contact">contact</h2>
</section>
#functions {
protected override void OnInit()
{
NavigateToElement();
UriHelper.OnLocationChanged += OnLocationChanges;
}
private void OnLocationChanges(object sender, string location) => NavigateToElement();
private void NavigateToElement()
{
var url = UriHelper.GetAbsoluteUri();
var fragment = new Uri(url).Fragment;
if(string.IsNullOrEmpty(fragment))
{
return;
}
var elementId = fragment.StartsWith("#") ? fragment.Substring(1) : fragment;
if(string.IsNullOrEmpty(elementId))
{
return;
}
ScrollToElementId(elementId);
}
private static bool ScrollToElementId(string elementId)
{
return JSRuntime.Current.InvokeAsync<bool>("scrollToElementId", elementId).GetAwaiter().GetResult();
}
}
index.html:
<script>
window.scrollToElementId = (elementId) => {
console.info('scrolling to element', elementId);
var element = document.getElementById(elementId);
if(!element)
{
console.warn('element was not found', elementId);
return false;
}
element.scrollIntoView();
return true;
}
</script>
Note: If you're using Blazor version .9.0, you should inject the IJSRuntime
Please, let me know if this solution works for you...
You can use 2 Nuget packages to solve this:
Scroll JS which is part of JsInterop Nuget. This has many feature see the docs but main part is IScrollHandler which is an injectable service. You can try it out with the demo app. You can see the scrollIntoView() and other JS functions wrapped, smooth scroll available. So much simpler to use JS scroll support...
Second option is to use "Parmalinks" in the URL you have "#". Nuget available here. It is what you requested. Basically it is using <a> tags but you don't have to bother with it (note even demo app has # URLs). Also renders "link" component with clickable icon and navigate/copy actions. Currenlty smooth scroll is not available but can be requested. You can try it out with the demo app.
I used Arron Hudon's answer and it still didn't work. However, after playing around I realized it wouldn't work with an anchor element: <a id='star_wars'>Place to jump to</a>. Apparently Blazor and other spa frameworks have issues jumping to anchors on the same page. To get around that I had to use a paragraph element instead (section would work too): <p id='star_wars'>Some paragraph<p>.
Example using bootstrap:
<button class="btn btn-link" onclick="document.getElementById('star_wars').scrollIntoView({behavior:'smooth'})">Star Wars</button>
... lots of other text
<p id="star_wars">Star Wars is an American epic...</p>
Notice I used bootstrap's btn-link class to make the button look like a hyperlink.
The same answer as the Issac's answer, but need to change some code.
I found the main problem is that you need it to be async. #johajan
#inject IJSRuntime JSRuntime
...
#functions {
protected override async Task OnInitAsync()
{
await base.OnInitAsync();
//NavigateToElement();
UriHelper.OnLocationChanged += OnLocationChanges;
}
private async Task OnLocationChanges(object sender, string location) => await NavigateToElement();
private async Task NavigateToElement()
{
var url = UriHelper.GetAbsoluteUri();
var fragment = new Uri(url).Fragment;
if(string.IsNullOrEmpty(fragment))
{
return;
}
var elementId = fragment.StartsWith("#") ? fragment.Substring(1) : fragment;
if(string.IsNullOrEmpty(elementId))
{
return;
}
await ScrollToElementId(elementId);
}
private Task<bool> ScrollToElementId(string elementId)
{
return JSRuntime.InvokeAsync<bool>("scrollToElementId", elementId);
}
}
I am wondering if you have any suggestions in regard to passing the string from a razor view to a React component?
export default class WidgetModel extends Component {
constructor() {
super()
}
render() {
const { } = this.props,
return (
<p>{Copy}</p>
)
}
}
<div id="widget">
<p>#vm.Copy</p>
</div>
As far as I understand, your c# code gets evaluated in the server and at that time, react doesn't exist, and then the code will be passed to the browser and then react will render it.
So basically you don't have access to your normal variables like you want, but It might be possible to assign your c# value to a global javascript variable so you can pick it up later in the browser :
Say this is your c# code :
<div id="widget">
<p>#vm.Copy</p>
</div>
add something like this :
<div id="widget">
<p>#vm.Copy</p>
<script>
var window.Copy = "#vm.Copy"; // we need the quotation so javascript doesn't compile it as a refrence, rather as a string
console.log('Copy',Copy);
</script>
</div>
and then in your react code your should be able to pick it up :
render() {
const { } = this.props,
return (
<p>{window.Copy}</p>
)
}