How to render a component passed as an attribute - c#

Assume Row is a component like so:
<div class="row">
<div class="col-6">
#Column1
</div>
<div class="col-6">
#Column2
</div>
</div>
#code
{
[Parameter]
public ComponentBase Column1 { get; set; }
[Parameter]
public ComponentBase Column2 { get; set; }
}
Then assume that we use this like so:
<Row Column1="foo" Column2="foo"></Row>
#code {
private readonly SomeComponent foo = new();
private readonly SomeComponent bar = new();
}
I'd expect a row containing two columns containing the rendered components for foo and bar, but instead I get two columns containing "Example.Shared.Components.SomeComponent"
How then do you render sub-components passed into other components? Is there a better way to do this (i.e. something like ng-content in Angular?
As a side note, I'm looking for solutions that allow this programatically (as above), and using markup (as below):
<Row>
<Column>
<Foo></Foo>
</Column>
<Column>
<Bar></Bar>
</Column>
</Row>

How about:
<div class="row">
<div class="col-6">
#Column1
</div>
<div class="col-6">
#Column2
</div>
</div>
#code
{
[Parameter]
public RenderFragment Column1 { get; set; }
[Parameter]
public RenderFragment Column2 { get; set; }
}
And then:
<Row>
<Column1>
<SomeComponent value="#somevalue"/>
</Column1>
<Column2>
<SomeOtherComponent/>
</Column2>
</Row>
As already stated, the Renderer instantiates all components and attaches them to the RenderTree.
Also note that all components don't necessarily inherit from ComponentBase, but they do all implement IComponent.

Blazor is mostly declarative. So you should:
<Row>
<Column1>
<Foo></Foo>
</Column1>
<Column2>
<Bar></Bar>
</Column2>
</Row>
You don't need to do new Foo() / new Bar()
Of course, you can take the very circuitous route of programmatically populating the RenderFragment. But it is generally not worth it. Instead, use Generic components and Polymorphism.

I'd recommend not thinking of filling objects with data and passing the objects around. I'd think of Components as the expression of existing data into the html space. Note how for each layer of data, I have nested Razor components. So you pass DATA into the Table component, not RenderFragments or any other kind of object. The following is simplified for clarity.
class TableData {
public List<RowData> Rows;
}
class RowData {
public List<ColumnData> Columns;
}
class ColumnData {
public string DisplayString;
}
TableComponent.razor
#foreach (var row in TableData.Rows){
<DataRowComponent Data=row />
}
#code {
[Parameter]
public TableData TableData{get; set;}
}
DataRowComponent.razor
#foreach (var column in Data.Columns ){
<DataColumnComponent Data=column />
}
#code {
[Parameter]
public RowData Data {get; set;}
}
DataColumnComponent.razor
<div>
#Data.DisplayString
</div>
#code {
[Parameter]
public ColumnData Data {get; set;}
}

What you want is templated components
You still can't use SomeComponent foo = new();.
Instantiation is left to the renderer. But consider that a good thing, it lets you slip in an (item) context.
You'll also want to look at Cascading values, to expose the Row to the Columns.
And finally, there are plenty of commercial and Open versions of a Blazor DataGrid out there. Don't reinvent the whole wheel.

Related

(Blazor) Select value not updating

I have a <select> element in my page, of which I want to get the value. To do that, I've added the #bind= property. Here's the code:
<form target="#">
<label for="subject">Vak</label>
<select #bind="#selectedSubject" id="subject">
#foreach (Subject subject in Auth.User.Subjects)
{
<option value="#subject.Uuid">#subject.Name</option>
}
</select><br />
<!-- (more input fields) -->
<button class="button btn-primary mt-3" #onclick="addNote">Toevoegen</button>
</form>
#code {
private String selectedSubject { get; set; }
private String inputTitle { get; set; }
private String inputContent { get; set; }
private void addNote()
{
Console.WriteLine(selectedSubject);
}
}
This doesn't output anything, and trying to use selectedSubject results in a NullReferenceError.
The other values (inputTitle, inputContent) work as expected.
Yes, the <select> is properly being filled with <option>s.
I've tried switching to whatever <EditForm> is, but it kept giving me warnings and failed to build.
First of all, you don't need a form at all. The main purpose of EditForm is to validate inputs-- like, making sure a password is the correct format.
Also, unless you want to programmatically SET the value of the dropdown, you don't need to bind to it-- instead, use #onchange:
<select #onchange=ProcessSelection>
. . .
#code {
Subject SelectedSubject {get; set;}
async Task ProcessSelection (ChangeEventArgs args){
SelectedSubject = Auth.User.Subjects.Single(s=>s.uuid = args.Value.ToString();
// Process the selection.
}
}
This will (1) give you a place to breakpoint for debugging. (2) Let you do useful things with the SelectedSubject object more easily (like add / remove it from a list, pass it as a parameter to a display component, etc.).
My approach to the problem is different from the other answer.
Here's everything in one Razor component.
Separate your data out into a class. Note that the fields can be null (you may not want them to be).
Use EditForm with normal binding. You can then do validation.
Logic in select list to determine if no value is selected and display a select message.
Separate out the Select code into a separate RenderFragment block to keep the code cleaner. Not necessary if you like all your markup in one place.
#page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
<EditForm EditContext="editContext">
<div class="col-12 mb-3">
<label class="form-label small muted">Subject</label>
<select class="form-select" #bind="model.Subject">
#this.SubjectList
</select>
</div>
<div class="col-12 mb-3">
<label class="form-label small muted">Title</label>
<input class="form-control" #bind="model.Title">
</div>
</EditForm>
<div class="alert alert-info m-3">
<div class="m-2">
Subject : #model.Subject
</div>
<div class="m-2">
Title : #model.Title
</div>
<div class="m-2">
Content : #model.Content
</div>
</div>
#code {
//private Note model = new() { Subject="Portugal"};
private Note model = new();
private EditContext? editContext;
protected override void OnInitialized()
=> this.editContext = new EditContext(model);
private IEnumerable<string> Subjects = new List<string> { "France", "Portugal", "Spain" };
private bool noSubjectSelected => !Subjects.Any(item => item == model.Subject);
// Separates out the code into a separate render block
private RenderFragment SubjectList => (__builder) =>
{
if (noSubjectSelected)
{
<option disabled selected value="">-- Select a Subject --</option>
}
foreach (var subject in Subjects)
{
<option value="#subject">#subject</option>
}
};
// Normally separate out into own class file
public class Note
{
public string? Subject { get; set; }
public string? Title { get; set; }
public string? Content { get; set; }
}
}
I've left the subject as a string as that's what you have it as. If it's a guid you will need to adjust the Select Code to handle a Guid/String pair.
Bear in mind that source code is an instruction of rendering intent so it isn't always clear how your code will react in all cases of operation.
You have to decide whether there will be a selection at all times, or whether no material selection is also valid. For example, if you want an option of "Select a Subject" at the top of the list which is the default, so set am option with that text an a value of Guid.Empty (all zeros). When processing values - just check that the selected Guid is not equal to Guid.Empty if you want to know if a "real" subject option is selected
I use the following two approaches with InputSelect. Note that the Microsoft renderer may set the text of the select to your bound option but may not set that option to "selected"
<InputSelect #bind-Value=MyData>
<option selected value="#Guid.Empty">Select a Subject</option>
#foreach (var i in Subjects)
{
<option value=#i.Id>#i.SubjectName</option>
}
</InputSelect>
If you want finer grained control over the select, there is this binding option. As well as a value to which you bind, you also need a function which is called on change - SelectChanged - where you can run other code. you should set the bound value within this method. If the InputSelect is inside a component, the bound value should also have an EventHandler of the same name with the "Changed" text appended - you should Invoke this in your SelectChanged function. Any parent control binding to the child will have its data automatically updated and the SetStateChanged is called for you
<InputSelect Value="#MyValue" ValueChanged="#((Guid value) => SelectChanged(value))" ValueExpression="#(() => MyValue)" >
#foreach (var i in MyValues)
{
if (TheSelectedValue == i.TheValue)
{
<option selected value=#i.TheValue>#i.ValueDisplay</option>
}
else
{
<option value=#i.TheValue>#i.ValueDisplay</option>
}
}
</InputSelect>
To solve your issue with things not changing, there are two ways you can do it:
Start with a null object for your SelectedSubject, do a null check in your click, and have a dummy select option that forces you to select an item:
.
<select #onchange=ProcessChange>
<option selected disabled>Pick a subject. . . </option>
#foreach (var subject in Auth.User.Subjects) {
<option>. . . </option>
}
</select>
<button #onclick=AddSubject />
#code {
List<Subject> SelectedSubjects = new();
Subject SelectedSubject {get; set; } = null;
async Task ProcessChange (ChangeEventArgs args){. . . }
async Task AddSubject{
if (Subject is not null) SelectedSubjects.Add (SelectedSubject);
}
}
Preselect your first option in your data initialization, and then you don't NEED the dummy <option> or the null check.
.
#code{
async Task OnInitializedAsync (bool IsFirstRender){
Auth.User.Subjects = await LoadSubjectsForUser (Auth.User);
SelectedSubject = Auth.User.Subjects.First();
}
}
In your use case, I think #1 is probably better.

Passing reference of component to its ChildContent components

To add some context, I'm trying to create a Dropdown select Blazor component. I've managed to create a concept of this entirely with CSS, #onclick, and #onfocusout.
I'm trying to pass a reference of the DropDown component to its children, DropDownItem. The only way I know how to achieve this, is by using the #ref and passing it as a parameter to the DropDownItem component.
<DropDown #ref="DropDownReference">
<DropDownItem ParentDropDown=#DropDownReference>Hello</DropDownItem>
<DropDownItem ParentDropDown=#DropDownReference>World</DropDownItem>
</DropDown>
There has to be a cleaner approach here that does not require manually passing the reference down to each child instance. I suppose I could use CascadingValue but that will still require me to store the DropDown reference.
I'm trying to notify DropDown parent when a click event occurs in DropDownItem. This will signal the parent to changes it selected value - as it would traditionally work in a select.
Here is an example of how you could do it using CascadingValue. The DropDownItem component will accept a [CascadingParameter] of type DropDown. There is nothing wrong in doing that, this is how it's done in most (if not all) component libraries.
DropDown.razor
<CascadingValue Value="this" IsFixed="true">
#* Dropdown code *#
<div class="dropdown">
#ChildContent
</div>
</CascadingValue>
#code {
[Parameter] public RenderFragment ChildContent { get; set; }
private string selectedItem;
public void SelectItem(string item)
{
selectedItem = item;
StateHasChanged();
}
}
DropDownItem.razor
#* Dropdown item code *#
<div class="dropdown-item" #onclick="OnItemClick">...</div>
#code {
[CascadingParameter] public DropDown ParentDropDown { get; set; }
[Parameter] public string Name { get; set; }
private void OnItemClick()
{
ParentDropDown.SelectItem(Name);
}
}
Usage:
<DropDown>
<DropDownItem Name="Hello">Hello</DropDownItem>
<DropDownItem Name="World">World</DropDownItem>
</DropDown>

Save components in list or array?

I have a index.razor, that has a for loop that generates let's say 100 components (those display text, and a couple more vlaues), now i want to hava a reference of every component, is there a way to save those in a List or an array?
I tryed it myself in many ways but I get the name of the component displayed in the html.
addPdfComp is executed by a button:
public void addPdfComp(int type)
{
var newComponent = new PdfComp();
newComponent.text = "some test text";
PdfComponentsList.Add(newComponent);
}
and to display the components:
#foreach (var comp in PdfComponentsList)
{
<div>#comp</div>
}
result:
see result
Edit:
The component itself:
<div class="row p-1 " style="border:solid; ">
<h5>TextComponent</h5>
<div>
<label>X</label>
<input type="number" class="col-2" #bind-value="_x"/>
<label>Y</label>
<input type="number" class="col-2" #bind-value="_y"/>
<label>Text</label>
<input type="text" class="col-4" #bind-value="text" />
</div>
#code {
[Parameter]
public string text { get; set; } = "Missing text";
[Parameter]
public string type { get; set; } = "noone";
[Parameter]
public int _x { get; set; } = 0;
[Parameter]
public int _y { get; set; } = 0;
}
The default string representation for an object is the fully qualified class name for that object, which is what you're seeing. This means that whatever PdfComp is, it doesn't have a meaningful .ToString() implementation.
And that's all Razor syntax really does in this case, it just emits output as a string. So #something will basically evaluate something as a string and write it to the output.
Based on a comment above:
My intention is [...] to display the component itself as if i just typed: <PdfComp></PdfComp> 100 times.
An instance of PdfComp in C# code is not the same thing as an instance of <PdfComp></PdfComp> in the markup. (Though I imagine they are very closely analogous somewhere in the depths of the framework.) What you want isn't a list of object instances, but a list of the data your view needs to render the markup.
So, structurally, more like this:
// PdfComponentsList here is a list of strings
PdfComponentsList.Add("some test text");
And then loop through it in the markup to display the elements:
#foreach (var pdf in PdfComponentsList)
{
<PdfComp text="#pdf"></PdfComp>
}
You could also make PdfComponentsList a list of some object to hold multiple properties, for example:
PdfComponentsList.Add(new SomeObject { Text = "some test text" });
And in the markup:
#foreach (var pdf in PdfComponentsList)
{
<PdfComp text="#pdf.Text"></PdfComp>
}

Blazor Closes the div tag on a conditionally div wrapper

Im trying to wrap one of my components on some specific tag based on some conditions.
To make it simple lets say that i have a
<div class="inner-class">
this must be in inner-class div and wrapped in some-class div
</div>
And if 'condition' == true then it should wrap it in another div the result should be like this
<div class="wrapper-class">
<div class="inner-class">
this must be in inner-class div and wrapped in some-class div
</div>
</div>
And before you say, use if-else method. i must say no. because the tag and the class in it is dynamic so i cant write it like that.
What i tried to do is
#if (condition)
{
#:<div class="wrapper-class">
}
<div class="inner-class">
this must be in inner-class div and wrapped in some-class div
</div>
}
#if (condition)
{
#:</div>
}
I Thought it should do the job.
But the problem is the browser closes the outer div before putting the inner div in it.
You have to use BuildRenderTree
With BuilderRenderTree you have full control to build component html.
For more information read this good article Building Components Manually via RenderTreeBuilder
I ended up writing a wrapper component similar to this to solve this problem pretty elegantly:
#if (Wrap)
{
<div id="#Id" class="#Class">
#ChildContent
</div>
}
else
{
#ChildContent
}
#code
{
[Parameter] public string? Id { get; set; }
[Parameter] public string? Class { get; set; }
[Parameter] public RenderFragment? ChildContent { get; set; }
[Parameter] public bool Wrap { get; set; }
}
It doesn't support dynamic tag types, but that would be pretty easy to create using a component implementing BuildRenderTree as mRizvandi suggested.
If it's a simple layout like you've described than you can make use of the MarkupString. The trick is understanding that MarkupString will automatically close any tags that are left open. Therefore you just need to build the entire string properly before trying to render it as a MarkupString.
#{ bool condition = true; }
#{ string conditionalMarkup = string.Empty; }
#if (condition)
{
conditionalMarkup = "<div class=\"wrapper-class\">";
}
#{ conditionalMarkup += "<div class=\"inner-class\"> this must be in inner-class div and wrapped in some-class div </div>"; }
#if (condition)
{
conditionalMarkup += "</div>";
#((MarkupString)conditionalMarkup)
}
else
{
#((MarkupString)conditionalMarkup)
}
I do this for building simple conditional markup. Usually inside of an object iteration making use of String.Format to fill in property values.

Blazor set value inside other component

I'm building my page and I was wondering if I can make my life easier and put some simple custom Input boxes inside method and them pass reference to my values to them
<div class="col-12 row">
<label class="col-2">#Caption</label>
<InputNumber class="form-control col-3" #bind-Value="#Value"/>
</div>
<br />
#code {
[Parameter]
public string Caption { get; set; }
[Parameter]
public int Value { get; set; }
}
And then use it like
<CustomInputNumber Caption="Price" Value="#Product.Price" />
It is possible to set value like that? Or pass object as reference? Thanks for help!
The way I would go about this is inheriting from inputbase and basically building your own input. Chrissainty has an excellent blog post about, which I think is much clearer then me citing half of what he already explains in that post.
https://chrissainty.com/building-custom-input-components-for-blazor-using-inputbase/
If however you really want to wrap the already existing inputcomponent, you could do it like this:
The component
<div class="col-12 row">
<label class="col-2">#Caption</label>
<InputNumber class="form-control col-3" Value="Value" TValue="int" ValueChanged="HandleValueChanged" ValueExpression="(()=>Value)" />
</div>
<br />
#code{
[Parameter]
public string Caption { get; set; }
[Parameter]
public int Value { get; set; }
[Parameter]
public EventCallback<int> ValueChanged { get; set; }
public void HandleValueChanged(int newValue)
{
ValueChanged.InvokeAsync(newValue);
}
}
Usage like:
<ExampleComponent #bind-Value="ExampleValue"/>
Here you basically override the existing events that exist on a default inputfield. When the default component notices a change, you call your own valuechanged event to pass the event to it's parent.
Though again, I think the first solution is much cleaner.

Categories

Resources