Blazor Dynamic Custom Elements in application - c#

I'm developing a webapp built in C# with Blazor WASM that is Asp.Net hosted. I'm making a blazor component that through the use of a library already in production, will generate a HTML fragment (or full embed) that is then displayed in this way
...
<div>
#((MarkupString)document)
</div>
...
with document containing the markup generated by the library.
As long as we're doing it with static content all is fine and dandy, but now we need to have some input in there that will then be sent back to the server to execute some actions.
In a MarkupString there is no way to include <InputFile /> or <InputText /> components in such a way that they are shown in the fragment and I can read their contents, and I can find no way to actually interact with the standard HTML tags, especially regarding the file upload.
Moreover we'll probably soon need to have a specific image uploader with preview which would be a custom Blazor component and this led me to the CustomElements .NET 7 feature that looks like what I need for both problems.
However I couldn't find how to actually implement this in my app, and the documentation I found is still very partial in that way. Is there a way to do what I need?
EDIT: Managed to fix this partially, with Chen's answer. I still have trouble with the binding though, as the #bind-Value directive is not working with
Unhandled exception rendering component: Microsoft.AspNetCore.Components.Forms.InputText requires a value for the 'ValueExpression' parameter. Normally this is provided automatically when using 'bind-Value'.
with this markup:
...
<custom-input-text #bind-value="$field1" name="$field1"></custom-input-text>
...
(the capital V in bind-Value becomes lowercase all by itself)
Am I doing something wrong again?

CustomElements should meet your requirements, you can create your own logic in Blazor components, and then use it in your application.
To use the component, you need to add the following JavaScript script references to your host app in this specific order.
<script src="_content/Microsoft.AspNetCore.Components.CustomElements/BlazorCustomElements.js"></script>
<script src="_framework/blazor.webassembly.js"></script>
You also need to add the corresponding middleware:
app.UseBlazorFrameworkFiles();
And use app.UseWebAssemblyDebugging(); for debugging.
Then you need to register the corresponding component in the Blazor program:
builder.RootComponents.RegisterCustomElement<Counter>("my-counter");
Then you can call this component in your application, including passing parameters, etc.
<my-counter title="Khalid" increment-amount="2" />
Here is a complete example with detailed explanation, you can use it as a reference.
Helpful links:
Blazor Custom Elements.
Using .NET 7's Blazor Custom Elements to render dynamic content.
ASP.NET Core Razor components.
Hope this can help you.
Edit1:
It looks like you can't do two-way binding between Razor Page and Razor Component. The official document mentions that parameters can be passed through JavaScript properties, but it doesn't seem to be able to read the parameters.
For example:
<my-counter ></my-counter>
<button onclick="Test()">click</button>
<script>
function Test()
{
const elem = document.querySelector("my-counter");
//get parameter failed, it's undefined
var text = elem.incrementAmount;
//successfully set parameter
elem.incrementAmount = "test";
}
</script>
So I'm guessing that CustomElements only have writable properties in Razor Page.
Edit2:
I found that JQuery can be used to detect the value of the input box. When getting this value and performing certain operations, can it also achieve the same effect as two-way binding?
<my-counter ></my-counter>
<button onclick="Test()">click</button>
<script>
function Test()
{
const elem = document.querySelector("my-counter");
//get the value of input
var value = $("my-counter").find("input").val();
elem.incrementAmount = "test";
}
</script>

Related

Convert Blazor HTML markup portion to string for email body

I have a simple Blazor component that take some inputs (some lists of object and few strings) and formats them into simple HTML for display, (tables generated from the lists of objects, simple text, etc.).
This HTML is a report that is intended to be both displayed to users in the app and also emailed to various people (via SendGrid). For compatibility, we are keeping the email HTML as simple as possible.
The component works fine, however I am not sure how to translate a component's markup portion into a simple string of escaped HTML so that I can pass the string to SendGrid and fire off an email.
I am aware of MarkupStrings, but I have only used them in reverse--to write a string containing HTML tags that will be properly displayed in my app. I can't find any suggestions for doing the conversion the way that I need it done.
Is there any simple way to have a component write all of its markup into a string so that I can email it out?
Or, would I be better off writing a .cs file with a static method that takes in the parameters in question, renders it into a MarkupString, and then passes the string both to SendGrid for email and also to a Blazor component for in-app display?
The simplest way to do that is to employ JSInterop to retrieve the Html markup for the component, produced by the browser. Let's say You've defined a child component, and you want to retrieve its html source. You can do that like this:
Define the child...
SelectGender.razor
<div id="selectGender">
<h1>Select Gender</h1>
<select>
#foreach (var gender in genders)
{
<option>#gender</option>
}
</select>
</div>
#code {
private List<string> genders = new List<string> { "Male", "Female", "Other" };
}
Usage
#page "/"
#inject IJSRuntime JSRuntime
<div>#((MarkupString) html)</div>
<SelectGender />
<button #onclick="GetHtml">Get Html</button>
#code{
private string html;
protected async Task GetHtml()
{
html = await JSRuntime.InvokeAsync<string>("myJsFunctions.getHtml");
}
}
_Host.cshtml
<script>
window.myJsFunctions =
{
getHtml: function () {
return document.getElementById("selectGender").innerHTML;
}
};
</script>
Is there any simple way to have a component write all of its markup into a string so that I can email it out?
No, your C# code has no simple way to do this - you could use JS Interop to get the rendered HTML from the dom, but nothing built in for it.
Or, would I be better off writing a .cs file with a static method that takes in the parameters in question, renders it into a MarkupString, and then passes the string both to SendGrid for email and also to a Blazor component for in-app display?
That is a possibility - I can't comment on the value of that to you, but it is a technique that could work if the component you are rendering is static,

Blazor server page with parameters throwing "404 not found " on manual refreshes

I have a Blazor Server app with a page that takes in parameters in the uri. When I click on an anchor tag that has the route setup to hit that page with params (like below) the link works fine, and the page loads.
<!--link content in here-->
However, if one tries to access the page from that url directly, or manually refreshes in the browser, then the page doesn't reinitialize or hit any breakpoints on the parameters. Instead, it throws a 404 not found.
So two things here:
Firstly, I'm confused about why it works fine from within the anchor tag, but dies any other way. Especially when pages without params in the #page directive work fine with refreshes/direct-urls.
Second, is this an intended behavior for Blazor Server or am I missing something here that's breaking the page refreshes/hitting-url-directly? Doesn't seem like a feature, but maybe I'm misunderstanding Blazor's routing.
Razor and Razor.cs for page in question:
#page "/MyPage/{Param1}/{Param2}"
<h1>MyPage</h1>
<Metrics Param1="#Param1" />
<Details Param1="#Param1" Param2="#Param2" />
<InProgress Param1="#Param1" Param2="#Param2" />
<InQueue Param1="#Param1" />
<br />
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;
using MyApp.Data.Models.ApiResponse;
namespace MyApp.Pages
{
public partial class MyPage
{
[Parameter]
public string Param1 { get; set; }
[Parameter]
public string Param2{ get; set; }
public TaskList Tasks { get; set; }
protected override Task OnInitializedAsync()
{
// work in progress, intend to do more here later on
var test = "";
return base.OnInitializedAsync();
}
}
}
Edit(s) -- per comment suggestions
UseEndpoints section of Configure method in Startup.cs:
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
After further digging I noticed that the #param2 will occaionally have a . char in it. Blazor does have a need for configuring routes that have params with dots in them. The below fall back doesn't work:
endpoints.MapFallbackToPage("/MyPage/{Param1}/{Param2}", "/MyPage");
It throws an:
InvalidOperationException: Cannot find the fallback endpoint specified by route values: { page: /MyPage, area: }.
I'm guessing that the area: being empty is a problem but I'm not finding how or where to properly set that. The example from the link shows just the Page's name in the fallback. Could someone please point out what's wrong with this fall back and how one can properly correct it?
My problem was with a dot character being in the parameter's value. When the routing issue is from a "dot" being in the parameter, then doing exactly what the docs recommends does the trick (go figure). You will want to specify a fall back for that particular route like so:
endpoints.MapFallbackToPage("/MyPage/{Param1}/{Param2}", "/_Host");
For WASM projects: you want to specify the html file vs _Host which should be Blazor Server specific.
endpoints.MapFallbackToFile("/MyPage/{Param1}/{Param2}", "index.html");
After this setup is applied, refreshing a page or reaching the URL directly should result in your application working as intended, no more 404 error different from the standard <NotFound> tag setup in the App.razor.
Key points:
Check if your url parameters have any known parsing exceptions like dots (Blazor assumes those are files being requested)
Use the default page of your application for the fall back, not the page you want to hit. In a Server app that's _Host for wasm it's index.html

How to pass data from asp.NET MVC to Angular2

What is the best way to pass data from an ASP.NET MVC controller to an Angular 2.0 component? For example, we use the ASP.NET MVC Model and would like to send a JSON version of it to Angular to use it in Angular.
When the controller is serving the view, we can already push some data to Angular2 (the model). So additional AJAX call to fetch that data is not required.
However, I am struggling to "inject" it into the Angular component. How do you do this? Any good references for this? As you may have noticed, I'm quite new to Angular2.
My index.cshtml looks like this.
<div class="container">
<div>
<h1>Welcome to the Angular 2 components!</h1>
</div>
<div class="row">
<MyAngular2Component>
<div class="alert alert-info" role="alert">
<h3>Loading...</h3>
</div>
</MyAngular2Component>
</div>
</div>
Kind regards,
Rob
The best way that I have found to pass data in from MVC (or any hosting/startup page) is to specify the data as an attribute on the bootstrapped component, and use ElementRef to retrieve the value directly.
Below is an example for MVC derived from the this answer, which states that it is not possible to use #Input for root-level components.
Example:
//index.cshtml
<my-app username="#ViewBag.UserName">
<i class="fa fa-circle-o-notch fa-spin"></i>Loading...
</my-app>
//app.component.ts
import {Component, Input, ElementRef} from '#angular/core';
#Component({
selector: 'my-app',
template: '<div> username: <span>{{username}}</span> </div>'
})
export class AppComponent {
constructor(private elementRef: ElementRef) {}
username: string = this.elementRef.nativeElement.getAttribute('username')
}
If you want to retrieve more complex data that is not suitable for an attribute, you can use the same technique, just put the data in a HTML <input type='hidden'/> element or a hidden <code> element, and use plain JS to retrieve the value.
myJson: string = document.getElementById("myJson").value
Warning: Access the DOM directly from a data-bound application such as Angular, breaks the data-binding pattern, and should be used with caution.
You might want to look for similar questions related to AngularJS, not Angular 2 specific, as the main gist of the thing remains the same:
you want your server-side Razor engine to render some kind of view (i.e. HTML or JS directly)
this view contains a JS template where part of the content is filled from a server model instance or anyway server data (e.g. a resource, dictionary, etc.)
in order to properly fill a JS variable from Razor, C# server-side data has to be properly serialized into a JSON format
In this post by Marius Schulz you can see as he serializes the data and uses that to fill a template AngularJS value component:
<script>
angular.module("hobbitModule").value("companionship", #Html.Raw(Model));
</script>
Something similar could be made to inject some data e.g. into window.myNamespace.myServerData, and then have Angular2 bootstrap that value among other providers.
In this post by Roel van Lisdonk, a similar approach is used, again, to fill an AngularJS-based template, with that ng-init directive:
<div ng-controller="main"
ng-init="resources={ textFromResource: '#WebApplication1.Properties.Resources.TextFromResource'}">
<h1>{{ title }}</h1>
{{ resources.textFromResource }}
</div>
As the first post points out, there's something to think about (pasting here):
A word of caution: The method I'm about to use is probably not a good fit for large amounts of data. Since the JavaScript data is inlined into the HTML response, it's sent over the wire every single time you request that page.
Also, if the data is specific to the authenticated user, the response can't be cached and delivered to different users anymore. Please keep that in mind when considering to bootstrap your Angular Apps with .NET data this way.
The second part may be less of an issue if your served page is already dynamic server-side, i.e. if it already has bits filled in out of server-side data.
HTH
You need to first bundle your services and controllers in separate module files and load services before controllers.
For example:
dist
|--services.js
|--controllers.js
Then you need to load the JavaScript code of the Services via ASP.NET MVC JavaScript result, here you need to inject your startup data.
public class ScriptController: Controller
{
public ActionResult GetServices(){
string file= File.ReadAllText(Server.MapPath("~dist/services.js"));
//modify the file to inject data or
var result = new JavaScriptResult();
result.Script = file;
return result;
}
Then in the index.html load the scripts as follows
<script src="/script/getservices"></script>
<script src="/dist/controller.js"></script>
This way you can inject data into angular code while loading.
However, even this has a performance impact due to time spent on fetching the view, compiling the view, and binding data to the view. For an initial load performance can still be improved if you use Server Side Rendering.
You can use the Input function exposed by #angular/core, I have for example an Angular 2 component to display information messages to the user of the application
My HTML template take an Angular 2 Message property
<div class="alert alert-info col-lg-10 col-lg-offset-1 col-md-10 col-md-offset-1 col-sm-10 col-sm-offset-1 col-xs-10 col-xs-offset-1">
<i class="fa fa-info-circle"></i> {{ Message }}
</div>
The Message property is passed as an input to my Angular 2 component named informationmessage.component.ts, for example
import { Component, Input } from '#angular/core';
#Component({
selector: 'informationmessage',
templateUrl: '../Templates/informationmessage.component.html'
})
export class InformationMessageComponent {
#Input() Message: string;
}
I then pass the data to my InformationMessageComponent using property binding in the HTML page, for example.
<informationmessage [Message]="InformationMessage"></informationmessage>
You can replace InformationMessage in the above example with the data that you get from your MVC controller for example
<informationmessage [Message]="#Model.InformationMessage"></informationmessage>
Please note: I did not test this scenario, but there is no technical reason for it not working, at the end of the day you are just binding a value to an Angular 2 property.
src/ApplicationConfiguration.ts
export class ApplicationConfiguration {
public setting1: string;
public setting2: string;
}
src/main.ts
declare var config : ApplicationConfiguration;
var providers = [{ provide: ApplicationConfiguration, useValue: config }];
platformBrowserDynamic(providers).bootstrapModule(AppModule)
.catch(err => console.log(err));
src/index.html
<script type="text/javascript">
var config = {setting1: "value1", setting2: "value2"};
</script>
src/app/app.component.ts
export class AppComponent {
private _config : ApplicationConfiguration;
constructor(config: ApplicationConfiguration) {
this._config = config;
}
}
I found a much simpler solution. Don't attempt to get the attribute from in the constructor! Use the ngOnInit() hook instead. The property will be accessible as long as it has been decorated with #Input(). It just appears that it is not available by the time the constructor is called.
Component HTML:
<MyComponent [CustomAttribute]="hello"></MyComponent>
Component TS:
export class MyComponentComponent {
#Input()
public CustomAttribute: string;
constructor() {}
ngOnInit() {
console.log("Got it! " + this.CustomAttribute);
}
}
Further to #Joseph Gabriel's approach, if you want a complex object to be passed via an attribute, in MVC you should serialize it to a string, and then in the angular side de-serialize it using JSON.Parse.

Storing parameterized partial-HTML pages for use from an ASMX service?

I have an ASMX service which serves some partial-HTML to a web-app as part of a JSON object. Until now, I've just been building the HTML in code, using StringBuilders. This is a huge pain since the formatting is really hard to read and I can't use any of Visual Studio's/Resharper's code completion, syntax highlighting, and other convenient features.
I tried to solve this with User Controls (I'm not committed to this approach if there is a better way. All I need are some very simple parametrized static partial HTML pages), but now I am running into problems when I try to render the control like this:
public override string Html
{
get
{
StringWriter writer = new StringWriter();
HtmlTextWriter htmlWriter = new HtmlTextWriter(writer);
CreateTestWizardPartials.TestPeriods testPeriodsHtml = new CreateTestWizardPartials.TestPeriods();
testPeriodsHtml.RenderControl(htmlWriter);
htmlWriter.Flush();
return writer.ToString();
}
}
This always returns an empty string. I've read you need to use Page.LoadControl() to dynamically load User Controls but there is no Page for me to use it with. Is there a workaround or a better solution than User Controls?
Thanks!
You could load static HTML file that contain the partial bits of markup you need. If you need to somehow bind the markup with dynamic data, then render the HTML files through a template engine.
Here's and older post regarding some template systems for ASP.Net:
Can you recommend a .net template engine?
And another more recent post regarding ASP.Net MVC:
JQuery's $ is in conflict with that of StringTemplate.Net in ASP.Net MVC

Error Message/Notification Component for .NET C# web app using jQuery (jGrowl)

I’m trying to create an error message / notification component for our .net C# web application. The requirements are that multiple messages can be posted, that they persist through partial post backs, and have different types (sticky, error, notification).
I have a server-side method that captures these messages, and I am using the popular jQuery plug-in jGrowl to display these messages. See below:
public void ShowErrorMessage(string Message)
{
Page.ClientScript.RegisterStartupScript(this.GetType(), "ErrorMessage", "<script type=\"text/javascript\">$.jGrowl('" + Message + "', { sticky: true });</script>");
}
Currently I am firing the jGrowl script inside the ShowErrorMessage method using RegisterStartupScript approach which works fine for the first message but of course the RegisterStartupScript does not allow me to to fire Javascript on a whim. How can I fire an instance of jGrowl each time ShowErrorMessage is hit?
My JavaScript knowledge is limited. I am open to any suggestions or alternative methods. Thank you.
Could you give a little more info. You mentioned doing partial postbacks. Are you doing a full postback or a partial? If partial, how so...through an updatepanel?
One thing to note is that the RegisterStartupScript will only register 1 instance of that script if it's always called from the same place. It's using this.GetType() and "ErrorMessage" to check to see if it's already been registered. If you call it with a different message, that script won't register since it's still the same type and "ErrorMessage".

Categories