look at this blazor server page:
#page "/"
<div>#Field1</div>
<div>#Field2</div>
<button #onclick="OnBtnClick">Btn</button>
#code
{
public String Field1 { get; set; }
public String Field2 { get; set; }
private async Task OnBtnClick()
{
Field1 = "test1";
// Value is displayed but i have not called StateHasChanged() there...
await Task.Delay(1000);
Field2 = "test2";
}
}
There is something i do not understand:
As you can see, i do not call StateHasChanged.
When i click on the button, i can see "test1" value displayed on the page, then wait 1 second and then i can see "test2".
I know StateHasChanged is automatically called after the event. But is it called automatically before the Task.Delay too ?
Thanks
There is a related question: when to call StateHasChanged() in Blazor component
It appears to indicate that StateHasChanged is automatically called in your example after the Task.Delay function is await-ed.
The other article says the Blazor "internal" event handler code looks like this:
var task = InvokeAsync(EventMethod);
StateHasChanged();
if (!task.IsCompleted)
{
await task;
StateHasChanged();
}
Related
I'm using .net6.0 Blazor razor library to create a component.
#using Microsoft.JSInterop;
#inject IJSRuntime _jsRuntime;
<audio id="#Id" src="#Source" />
#code {
[Parameter]
public string Id { get; set; }
[Parameter]
public string Source { get; set; }
}
I use this component in a razor page using this :
<AudioPlayer
Id="reactorAudioElement1"
Source="/audio/lion-roaring.ogg">
</AudioPlayer>
And everything is doing fine at this point.
But, if I try to use #ref like that,
<AudioPlayer
#ref=#_Jukebox2>
</AudioPlayer>
#code {
private AudioPlayer _Jukebox2;
protected override void OnInitialized()
{
_Jukebox2 = new AudioPlayer()
{
Id="reactorAudioElement2",
Source="/audio/Bleep_02.ogg"
};
}
}
nothing is set in the DOM.
But I can read data like this, and I cannot set it...
<AudioPlayer
#ref=#_Jukebox2
Id="reactorAudioElement2"
</AudioPlayer>
#code {
private AudioPlayer _Jukebox2;
protected override void OnAfterRender(bool firstRender)
{
Console.WriteLine(_Jukebox2.Id); //ok
_Jukebox2.Source = "toto.mp3"; //doesn't do anything
}
}
Adding a StateHasChanged(); is not working
What am I doing wrong ?
Your component in DOM is created with this section of code, and your component will have valid #ref value
<AudioPlayer
#ref=#_Jukebox2
Id="reactorAudioElement2"
</AudioPlayer>
but you are creating another object in code with this section of code
_Jukebox2 = new AudioPlayer()
{
Id="reactorAudioElement2",
Source="/audio/Bleep_02.ogg"
};
In above code you can create object, but can not have DOM reference
This part of code will create an element in DOM and set it's reference to _Jukebox2, will also set value of Source
<AudioPlayer
#ref=#_Jukebox2
Id="reactorAudioElement2"
Source="#source"
</AudioPlayer>
private string source="Source="/audio/Bleep_02.ogg"`;
And in code behind you should be able to access public methods or variable in _Jukebox2 like this
_Jukebox.Source="toto.mp3";
If its not updating DOM then issue might be somewhere else
I'm rendering components using DinamicComponent and I need to call a function found in the child component.
I can't find the equivalent of using #ref for the DinamicComponents so that I can reference to call the function.
This is the parent component
<div class="tab-content">
#foreach (VerticalTabComponent.TabModel oneTabItem in VerticalTabsList)
{
<div class="tab-pane fade show #(oneTabItem.TabIndex == SelectedTabIndex ? "active" : "")" #key=#($"VTabDivDynamic_{TabPrefix}_{oneTabItem.TabIndex.ToString()}")>
<DynamicComponent
Type=#System.Type.GetType(oneTabItem.TabComponent)
Parameters=#oneTabItem.TabParameters>
</DynamicComponent>
</div>
}
</div>
This is the code in Blazor Component Tab
public partial class TabComponent
{
[Parameter]
public EventCallback<string> InsertUpdateCallback { get; set; }
protected override async Task OnInitializedAsync()
{
await CallAnyfunctionAsync();
}
private async Task<bool> LoadDataGrid()
{
//this is the function I need to call from parent
}
}
How can I call the Load Grid function from the parent component?
There is an easy solution. Not sure if that is new but the ref-attribut does exist for the DynamicComponent! You can use it like this:
<DynamicComponent Type="typeof(MyComponent)" Parameters="#MyParameters" #ref="dc" />
and in Code-Behind:
private DynamicComponent? dc;
private MyComponent? MyComponentRef
{
get
{
return (MyComponent?)dc?.Instance;
}
}
Normally in Blazor we use #Ref to get a reference to a component, but as you've seen this won't work with a DynamicComponent.
A workaround for this would be to add a [Parameter] to the component called something like Register which is an action with the generic type set as the component type. You can then add code to handle OnParametersSet to call this method in the component.
You can then add a Register parameter in your TabParameters which gets updated with a reference.
Example code below would be added to the SurveyPrompt component:
/// <summary>
/// will be called when params set
/// </summary>
[Parameter] public Action<SurveyPrompt> Register { get; set; }
protected override void OnParametersSet()
{
if (Register != null)
{
// register this component
Register(this);
}
}
You add a Register parameter with an Action<type> value. Here's an example:
SurveyPrompt sp1 = null;
void Register1(SurveyPrompt survey)
{
sp1 = survey;
Console.WriteLine("SP1 has title " + sp1.Title);
}
protected override void OnInitialized()
{
Action<SurveyPrompt> p1 = Register1;
params1 = new Dictionary<string, object>()
{
{ "Title", "Survey Title Here" },
{ "Register", p1 }
};
}
IDictionary<string, object> params1;
I have a Language dropdown in Blazor layout that finally calls this method:
private async Task SetLanguage(string lang)
{
cultureChanger.ChangeCulture(lang);
await GetLanguageValues();
}
The GetLanguageValues() method should be called then, But the debugger does not step over cultureChanger.ChangeCulture(lang); and then goes out of the method to the caller. How do I solve this?
GetLanguageValues method:
string lang = "";
private async Task GetLanguageValues()
{
lang = textService.GetText(cultureChanger.Current, "lang");
ViewData.Language = lang;
StateHasChanged();
}
CultureChanger class:
public class CultureChanger
{
public CultureInfo Current { get; private set; } = new CultureInfo("en");
public event Action OnCultureChange;
public void ChangeCulture(string cultureName)
{
Current = new CultureInfo(cultureName);
OnCultureChange?.Invoke();
}
}
I am upgrading a preview 6 Blazor project to preview 9, Previously it was working.
Solved:
One of the components that subscribe to this event had StateHasChanged() in its method that I replaced with: base.InvokeAsync(StateHasChanged);
I am trying to create a custom error code page that displays a message I pass to it in my .NET Core MVC 1.1 application. I setup custom error code pages support in the Startup.cs class file and then created a simple view in a controller that does public IActionResult Example1 => NotFound("Some custom error message"). I expected this message to be pushed to the controller however this is not the case. Calling NotFound() without any parameters hits the error controller but as soon as I pass a message through, the controller is never used and a simple text message is displayed.
I could have sworn I used to do this in the past with classic .NET MVC but it has been awhile.
How can I have custom error code pages that display the proper error. I also need the ability in a controller to return the standard text or JSON response during the error for cases when I expect a JSON response (API actions and such). I am assuming there is a way to do this with a attribute but I have yet to find a way to do either of these tasks.
What you could do is something similar to how the StatusCodePages middleware works. That middleware allows a pipeline re-execution model, to allow handling status code errors through the normal MVC pipeline. So when you return a non-successful status code from MVC, the middleware detects that and then re-executes the whole pipeline for a status code error route. That way, you are able to fully design status code errors. But as Chris Pratt already mentioned, those status codes are typically limited to just their code. There is not really a way to add additional details to it.
But what we could do is create our own error handling implementation on top of that re-execution model. For that, we create a CustomErrorResponseMiddleware which basically checks for CustomErrorResponseException exceptions and then re-executes the middleware pipeline for our error handler.
// Custom exceptions that can be thrown within the middleware
public class CustomErrorResponseException : Exception
{
public int StatusCode { get; set; }
public CustomErrorResponseException(string message, int statusCode)
: base(message)
{
StatusCode = statusCode;
}
}
public class NotFoundResponseException : CustomErrorResponseException
{
public NotFoundResponseException(string message)
: base(message, 404)
{ }
}
// Custom context feature, to store information from the exception
public interface ICustomErrorResponseFeature
{
int StatusCode { get; set; }
string StatusMessage { get; set; }
}
public class CustomErrorResponseFeature : ICustomErrorResponseFeature
{
public int StatusCode { get; set; }
public string StatusMessage { get; set; }
}
// Middleware implementation
public class CustomErrorResponseMiddleware
{
private readonly RequestDelegate _next;
private readonly string _requestPath;
public CustomErrorResponseMiddleware(RequestDelegate next, string requestPath)
{
_next = next;
_requestPath = requestPath;
}
public async Task Invoke(HttpContext context)
{
try
{
// run the pipeline normally
await _next(context);
}
catch (CustomErrorResponseException ex)
{
// store error information to be retrieved in the custom handler
context.Features.Set<ICustomErrorResponseFeature>(new CustomErrorResponseFeature
{
StatusCode = ex.StatusCode,
StatusMessage = ex.Message,
});
// backup original request data
var originalPath = context.Request.Path;
var originalQueryString = context.Request.QueryString;
// set new request data for re-execution
context.Request.Path = _requestPath;
context.Request.QueryString = QueryString.Empty;
try
{
// re-execute middleware pipeline
await _next(context);
}
finally
{
// restore original request data
context.Request.Path = originalPath;
context.Request.QueryString = originalQueryString;
}
}
}
}
Now, all we need to do is hook that up. So we add the middleware within our Startup.Configure, somewhere near the beginning:
app.UseMiddleware<CustomErrorResponseMiddleware>("/custom-error-response");
The /custom-error-response is the route that we are re-executing when a custom response is being requested. This can be a normal MVC controller action:
[Route("/custom-error-response")]
public IActionResult CustomErrorResponse()
{
var customErrorResponseFeature = HttpContext.Features.Get<ICustomErrorResponseFeature>();
var view = View(customErrorResponseFeature);
view.StatusCode = customErrorResponseFeature.StatusCode;
return view;
}
Since this uses MVC, this also needs a view:
#model ICustomErrorResponseFeature
#{
ViewData["Title"] = "Error";
}
<p>There was an error with your request:</p>
<p>#Model.StatusMessage</p>
And that’s basically all. Now, we can just throw our custom error response exceptions from our MVC actions to trigger this:
// generate a 404
throw new NotFoundResponseException("This item could not be found");
// or completely custom
throw new CustomErrorResponseException("This did not work", 400);
Of course, we could also expand this further, but that should be the basic idea.
If you are already using the StatusCodePages middleware, you might think whether all this custom re-execution is really necessary, when you already have exactly that in the StatusCodePages middleware. And well, it is not. We can also just expand on that directly.
For that, we will just add the context features, which we can set at any point during the normal execution. Then, we just return a status code, and let the StatusCodePages middleware run. Inside its handler, we can then look for our feature and use the information there to expand the status code error page:
// Custom context feature
public interface IStatusCodePagesInfoFeature
{
string StatusMessage { get; set; }
}
public class StatusCodePagesInfoFeature : IStatusCodePagesInfoFeature
{
public string StatusMessage { get; set; }
}
// registration of the StatusCodePages middleware inside Startup.Configure
app.UseStatusCodePagesWithReExecute("/error/{0}");
// and the MVC action for that URL
[Route("/error/{code}")]
public IActionResult StatusCode(int code)
{
var statusCodePagesInfoFeature = HttpContext.Features.Get<IStatusCodePagesInfoFeature>();
return View(model: statusCodePagesInfoFeature?.StatusMessage);
}
Inside of the normal controller actions, we can set that feature before returning a status code:
HttpContext.Features.Set<IStatusCodePagesInfoFeature>(new StatusCodePagesInfoFeature
{
StatusMessage = "This item could not be found"
});
return NotFound();
It is too bad you cannot intercept NotFound, Unauthorized, etc. responses in a middleware class.
Okay, option three! You can totally intercept those responses, just not inside of middleware, since these are MVC results and will not leave the MVC pipeline. So you have to intercept them within the MVC filter pipeline. But we could absolutely run a filter, for example a result filter, that modifies the result.
The problem is that we still need a way to pass the information on. We could use a context feature again, but we can also use the MVC object results. So the idea is that we can just do the following in the MVC actions:
return NotFound("The item was not found");
So usually, that string would be the plain text response. But before the result is being executed and the response is being generated, we can run a result filter to modify this and return a view result instead.
public class StatusCodeResultFilter : IAsyncResultFilter
{
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
// retrieve a typed controller, so we can reuse its data
if (context.Controller is Controller controller)
{
// intercept the NotFoundObjectResult
if (context.Result is NotFoundObjectResult notFoundResult)
{
// set the model, or other view data
controller.ViewData.Model = notFoundResult.Value;
// replace the result by a view result
context.Result = new ViewResult()
{
StatusCode = 404,
ViewName = "Views/Errors/NotFound.cshtml",
ViewData = controller.ViewData,
TempData = controller.TempData,
};
}
// intercept other results here…
}
await next();
}
}
All you need is a view at Views/Errors/NotFound.cshtml now and everything will magically work once you have the filter registered.
You can either register the filter by adding a [TypeFilter(typeof(StatusCodeResultFilter))] attribute to the controller or individual actions, or you can register it globally.
What you want is not possible. When you do something like return NotFound with a message, that message will be included in the response body only if it's left unmolested. When you do something like enable status code pages, the NotFound is simply caught by the middleware, and the request will simply be handed off to your error handling action to ultimately obtain the response. Importantly, that means your original NotFoundResult along with any custom message has been round-filed.
Here is how it works:
Filter web part sends row of data to all other webparts on the page.
It's control is rendered at load time, rendering the control selects which row is sent back to the other webparts on the page.
This causes the issue on the first page load where the other webparts will request the row from provider before it has finished loading and therefore has no information to provide yet.
The only solution (which is really ugly, slow and horrible) is to run all of the code that would be run in the control class the webpart uses in the webpart's constructor and use it to predict what values the control will have on the first run. This also leads to a whole bunch of issues with deploying that I really would rather avoid.
Here's the webpart code:
public class FilterProjectHeader : WebPart, IWebPartRow
{
// Visual Studio might automatically update this path when you change the Visual Web Part project item.
private const string _ascxPath = #"[link goes here]";
public DataRowView data;
public DataTable table;
private FilterProjectHeaderUserControl control;
public FilterProjectHeader()
{
//Code I want to avoid using:
//var web = SPContext.Current.Web;
//table = web.Lists["foo"].Items.GetDataTable();
//data = foo();
}
protected override void CreateChildControls()
{
control = Page.LoadControl(_ascxPath) as FilterProjectHeaderUserControl;
control.provider = this;
Controls.Add(control);
}
public PropertyDescriptorCollection Schema
{
get
{
return TypeDescriptor.GetProperties(table.DefaultView[0]);
}
}
[ConnectionProvider("Row")]
public IWebPartRow GetConnectionInterface()
{
return this;
}
public void GetRowData(RowCallback callback)
{
callback(data);
}
}
And for the control:
public partial class FilterProjectHeaderUserControl : UserControl
{
public FilterProjectHeader provider { get; set; }
private String _selectedValue;
//Both OnLoad and OnInit have the same result.
protected override void OnInit(EventArgs e)
{
//This is what gets run the first time:
if (!IsPostBack)
{
//Code here finds data then sends it back to webpart like this:
//All of the code in this method definitely does run; I have stepped
//through it and it works but it seems to happen too late to have any
//effect.
provider.data = item;
provider.table = profilesTable;
}
}
protected void filterDropDown_SelectedIndexChanged(object sender, EventArgs e)
{
//Post back method code exempted... it works.
provider.data = item;
provider.table = profilesTable;
}
So after a lot of time working with this, I found the issue is actually with what Microsoft recommends to do as best practice (they say to always use CreateChildControls to load controls onto the page).
CreateChildControls runs AFTER OnLoad when it is the first time a page is loading, but runs BEFORE OnLoad on a repost.
This is why it works on reposts, but not on first page load.
Switching CreateChildControls to OnInit solves the problem, because OnInit will always run before OnLoad.