refer to components that created by loop - c#

I want to create a components by following this steps:
I have a list of items.
I want to loop in this list and create a component like InputNumber.
Add EventCallback to the generic created InputNumber that accept ref of this Inputtext because I want to use this ref to set the focus on this InputNumber.
I have also onblure method that execute some code for me, and I am using the onfocus to return focus to the input after execute this code by onblure
My question How can I get this ref and send it as parameter of EventCallback? The problem here that this components have been generated by loop, so I don't want to create by hand hundred variables to represent ref's.
My concept code like this:
#code{
private void OnFocus(MyInputNumber<double?> obj)
{
if (obj is not null)
{
obj!.Element.Value.FocusAsync();
}
}
}
#foreach(var MyItem in MyList)
{
<EditForm Model="MyItem">
//Some components ..
<label>
Test
<InputNumber #bind-Value="MyItem.MyVal"
#onfocus="#((InputNumber<double?> obj #*wrong*#) =>
OnFocus(obj))"
#onblur=#(() => OnblureHandler(context))
</label>
</EditForm>
}
If you see up the parameter InputNumber<double?> obj, this way is wrong, usually I use #ref=SomeVariable but becasue I created in generic way, I can not do that.
Note:
I don't to adjust my list to be dictionary<MYItemType,InputNumber<double?>>, or create a new class that has InputNumber<double?> as property. I am searching for different way, like go from editcontext to any input has been modified and reset focus on it, I don't know if that possible !

You can add an InputNumber<double?> InputNumberRef { get; set; } property to your model class. Then is the foreach loop bind it to the component reference #ref="MyItem.InputNumberRef" then you can pass it in your callback method #onblur="() => HandleBlur(MyItem.InputNumberRef)".
Here is the demo code that I used. The following code after input onblur event it waits 2 seconds and returns the focus to the input.
#page "/"
#foreach (var item in _items)
{
<EditForm Model="#item">
<InputNumber class="form-control" #ref="#item.InputNumberRef" #bind-Value="#item.Value" #onblur="() => HandleBlur(item.InputNumberRef)" />
</EditForm>
}
#code {
private List<Item> _items = new List<Item>
{
new Item { Value = 10 },
new Item { Value = 30 },
new Item { Value = 20 },
};
private async Task HandleBlur(InputNumber<int> inputNumberRef)
{
if (inputNumberRef.Element.HasValue)
{
await Task.Delay(2000);
await inputNumberRef.Element.Value.FocusAsync();
}
}
public class Item
{
public int Value { get; set; }
public InputNumber<int> InputNumberRef { get; set; }
}
}
Credits to user #enet for suggesting this solution in a different question on stackoverflow.

If your requirement is that you apply some form of complex validation on the content of the input before the user is allowed to leave it, i.e if the handler attached to onBlur fails validation then you want to return focus to the input, then this is how to do that without resorting to dictionaries, ...
I've defined a custom InputText component to demonstrate the principles. You'll need to apply the same principles to any other InputBase component where you want to apply the functionality. The key is defining a delegate Func (which returns a bool) as a parameter which is called when the user tries to leave the control. As everything is contained within the component (a bit of SOLID as pointed out by #BrianParker), we can call the inbuilt Element property to return focus to the component.
#inherits InputText
<input #ref="Element"
#attributes="AdditionalAttributes"
class="#CssClass"
value="#CurrentValue"
#oninput="OnInput"
#onblur="OnBlur" />
#if (validationMessage != string.Empty)
{
<div class="validation-message">
#validationMessage
</div>
}
#code {
private string validationMessage = string.Empty;
[Parameter] public Func<string?, Task<bool>>? BlurValidation { get; set; }
[Parameter] public string ValidationFailMessage { get; set; } = "Failed Validation";
private void OnInput(ChangeEventArgs e)
=> this.CurrentValueAsString = e.Value?.ToString() ?? null;
private async Task OnBlur(FocusEventArgs e)
{
validationMessage = string.Empty;
if (Element is not null && BlurValidation is not null && !await this.BlurValidation.Invoke(this.CurrentValue))
{
await Element.Value.FocusAsync();
validationMessage = ValidationFailMessage;
}
}
}
And a demo page:
#page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
#foreach(var item in model)
{
<EditForm Model=item>
<MyInputText class="form-text" #bind-Value=item.MyCountry BlurValidation=CheckBlur />
</EditForm>
}
#code {
private List<MyData> model = new List<MyData>() { new MyData { MyCountry = "UK" }, new MyData { MyCountry = "Australia" } };
private async Task<bool> CheckBlur(string value)
{
// Emulate some async behaviour to do whatever checking is required
await Task.Delay(100);
// simple test here to demonstrate - I know you could use nornal validation to do this!
return value.Length > 5;
}
public class MyData
{
public string? MyCountry { get; set; }
}
}
I'm not sure I'm happy with the UX using this design, but it's your code.

Related

Blazor - Binding Blazor Component or workaround [ask from WPF guy]

I am looking for something like this in the WPF for Blazor, if is it possible:
WPF:
<ContentPresenter Content="{Binding LicensesText}" />
public LicensesText LicensesText { get; set; }
ctor()
{
LicensesText licensesText = new LicensesText();
licensesText.ClickedShowComparision += () => { Licenses = new Licences(); };
LicensesText = licensesText;
//Sleep for 5 seconds
List<LicensesText> list = new ();
list.Add(licensesText);
LicensesText licensesText1 = new LicensesText();
licensesText1.ClickedShowComparision += () => { Licenses = new Licences(); };
LicensesText = licensesText1;
//Sleep for 5 seconds
LicensesText = list.First();}
}
The Best would be something like this:
BLAZOR:
SaySomething.razor
<h3>#Label</h3>
#code {
public string Label { get; set; } = "Default Label";
public void SetLabel(string label)
{
Label = label;
StateHasChanged();
}
}
Index.razor
#SaySometing
#code
{
public SaySometing SaySometing { get; set; } = new SaySometing();
}
But for sure it doesn't work like it.
I come to some solution but all of them are imperfect. And I dont know how to visualize for example already created ComponentBase element, if is it possible.
#page "/"
#DynamicRender
<SaySometing #ref="SaySometing" />
<DynamicComponent Type="typeof(SaySometing)" #ref="DynamicComponent"></DynamicComponent>
<button #onclick="args => ChangeText(args)">Click Me!</button>
#code
{
public SaySometing SaySometing { get; set; } = new SaySometing();
public SaySometing SaySometing1 { get; set; } = new SaySometing();
public DynamicComponent DynamicComponent { get; set; } = new DynamicComponent();
private RenderFragment DynamicRender { get; set; }
protected override Task OnInitializedAsync()
{
DynamicRender = CreateComponent();
return base.OnInitializedAsync();
}
private RenderFragment CreateComponent() => builder =>
{
builder.OpenComponent(0, typeof(SaySometing));
builder.AddComponentReferenceCapture(1, obj =>
{
SaySometing1 = (SaySometing) obj;
});
builder.CloseComponent();
};
private void ChangeText(MouseEventArgs args)
{
SaySometing.SetLabel("Hello From SaySomething!");
SaySometing1.SetLabel("Gutten Tag From SaySomething1!");
((SaySometing?)DynamicComponent?.Instance)?.SetLabel("DobrĂ½ den from DynamicComponent");
StateHasChanged();
}
}
All three works properly and switch text to the right SaySomething.Label. But what if i want to change the SaySometing1 to new instance and keep the old instance for later and then again render it? Or how is it solved in the Blazor. As i told, I was the WPF guy and it is a new jungle for me. Or the best workaround in this technology.
You need to separate out the data and it's state from it's presentation. Many components demonstrate no separation of concerns: data access, data management and state, and data display are all rolled into a page.
This may be totally wide of the mark: I'll remove the answer if it is.
Take the Counter.
We can create a counter state object:
public class CounterState
{
public Guid CounterId { get; } = Guid.NewGuid();
public int Counter { get; private set; }
public string Name => this.CounterId.ToString().Substring(0, 4);
public void Increment()
=> Counter++;
}
A Service to manage the data - in this case a list of counter objects:
public class CounterViewService
{
public CounterState CounterState { get; private set; }
public List<CounterState> Items { get; set; } = new();
public event EventHandler? CounterChanged;
public CounterViewService()
{
this.CounterState = new CounterState();
Items.Add(this.CounterState);
}
public void SetCounter(CounterState item)
{
this.CounterState = item;
this.CounterChanged?.Invoke(item, EventArgs.Empty);
}
public void AddCounterState()
{
var item = new CounterState();
this.Items.Add(item);
this.CounterState = item;
}
public void Increment()
{
this.CounterState.Increment();
this.CounterChanged?.Invoke(this.CounterState, EventArgs.Empty);
}
}
Registered as a service like this:
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddScoped<CounterViewService>();
builder.Services.AddSingleton<WeatherForecastService>();
A Counter Component to display counter state:
#inject CounterViewService Service
#implements IDisposable
<div class="bg-dark text-white p-2 m-2">
<h3>Counter Display #Service.CounterState.Name</h3>
<div>Counter : #Service.CounterState.Counter</div>
<div class="m-2">
<button class="btn btn-light" #onclick="IncrementCount">Increment me</button>
</div>
</div>
#code {
protected override void OnInitialized()
=> this.Service.CounterChanged += this.OnChange;
private void IncrementCount()
=> this.Service.Increment();
private void OnChange(object? sender, EventArgs e)
=> this.StateHasChanged();
public void Dispose()
=> this.Service.CounterChanged -= this.OnChange;
}
And finally the display page:
#inject CounterViewService Service
#page "/counter"
<PageTitle>Counter</PageTitle>
<CounterDisplay />
<div class="m-2">
<button class="btn btn-primary me-2" #onclick=this.AddCounter>AddCounter</button>
#foreach (var counter in Service.Items)
{
<button class="btn btn-secondary me-2" #onclick="() => this.SelectCounter(counter)">Select Counter #(counter.CounterId.ToString().Substring(0, 4))</button>
}
</div>
#code {
private void AddCounter()
=> this.Service.AddCounterState();
private void SelectCounter(CounterState counter)
=> this.Service.SetCounter(counter);
}
A screenshot to show it in action:
Here is what exactly I need...
RazorParent.razor:
//Soemthing like ContentPresenter in the WPF
< DoesExistSomethingLikeThis Content="#RazChild" />
<a #OnClick="DisplayConfiguredRazChild">...
<a #OnClick="DisplayEmptyRazChild">...
RazorParent.razor#code:
RazorChildren RazChild {get;set;} = null;
void DisplayConfiguredRazChild()
{
RazorChildren razChild = new RazorChildren();
razChild.Configure();
RazChild = razChild;
StateHasChanged();
}
void DisplayEmptyRazChild()
{
RazorChildren razChild = new RazorChildren();
RazChild = razChild;
StateHasChanged();
}
Or if exist something like you create view and configure it in the code behind and then display it through the binding and still control it over the created instance... Or you must go always through the:
<RazorChildren #ref="_razChildRef" Param1="#RazChild.Param1"/>
Where you unfortunately don't control the original instance. Is it possible to create something like WPF ContentPresenter in the Blazor? Where you just bind the ComponentBase instance? It would be amazing, I know there are RenderFragment, DynamicComponent etc... But nothing can substitude this behavior where you just bind directly the ComponentBased component... Without duplicate reference instances and passing parameters in razor.

How to pass method name at runtime to component generated by DynamicComponent in Blazor

I'm trying to render dynamic content in Blazor Wasm using DynamicComponent from .Net 6. The component to render is defined in a JSON file with the following structure.
{
"type":"MyButton",
"parameters": {
"Label":"My Buttom",
"OnClicked": "DoAction"
}
}
DynamicComponent allows us to pass parameters to the rendered Component using Dictionary<string, object> like the code below where we can define component.parameters as Dictionary<string,object>
<DynamicComponent Type=#component.type Parameters=#component.parameters />
In my razor file, I have defined the "DoAction()" method. I want the this method to be called when the MyButton component is clicked. But how can we pass this DoAction() method as EventCallBack to the rendered Component?
MyButton.razor component:
<button class="btn btn-primary" #onclick="HandledClicked">#LabelText</button>
#code{
[Parameter]
public string LabelText {get; set;}
[Parameter]
public EventCallback OnClicked { get; set; }
private void HandleClicked()
{
OnClicked.InvokeAsync();
}
}
DynamicPage.razor: (Update #1)
#page "/DynamicPage"
#foreach (var component in components)
{
<DynamicComponent Type="component.Type" Parameters="component.Parameters" />
}
#code{
List<JsonComponent> components = new();
protected async override Task OnInitializedAsync()
{
var jsonComponentList = await Http.GetFromJsonAsync<List<JsonComponent>>("/data.json");
foreach (var item in jsonComponentList)
{
JsonComponent componentItem = new();
componentItem.Type = Type.GetType($"{nameSpaceComponents}{item.Type}");
componentItem.Parameters = new();
// below code to populate the component parameters as Dictionary<string, object>.
// the problem here is how to pass "DoAction()" method which is defined in the
// Json file to the rendered component by adding it to the paramater Dictionary<string, object>?
foreach (var kvp in item.Parameters)
{
var jsonElement = ((JsonElement)kvp.Value).GetString();
componentItem.Parameters.Add(kvp.Key, jsonElement);
}
}
}
public void DoAction()
{
//.. codes to perform some custom logic here when Button component is clicked.
}
}
JsonComponent.cs Class:
public class JsonComponent
{
public JsonComponent()
{
}
public Type Type { get; set; }
public Dictionary<string, object> Parameters { get; set; }
}
In order to pass the method or function at runtime to the dynamically generated Component via the Dictionary<string, object> Parameters, we need to create an EventCallBack by using EventCallback.Factory.Create() method.
I use the foreach loop to get the list of parameters defined in the Json file that we need to add into the Dictionary<string, object> Parameters.
Then I have an if condition to check whether the parameter to be passed to the Component is an Event. Here I predefined all Event Key name should prefix with 'On' e.g. 'OnClicked'. If the parameter is an Event then we create an EventCallback
callback = EventCallBack.Factory.Create<string>(this, (Action<string>) this.GetType().GetMethod(kvp.Value.ToString()).CreateDelegate(typeof(Action<string>), this));
componentItem.Parameters.Add(kvp.Key, callback);
Below is the codes for the DynamicPage.razor:
#page "/DynamicPage"
#foreach (var component in components)
{
<DynamicComponent Type="component.Type" Parameters="component.Parameters" />
}
#code{
List<JsonComponent> components = new();
EventCallBack<string> callback; // Declare a EventCallBack variable use to pass to the generated Component
protected async override Task OnInitializedAsync()
{
var jsonComponentList = await Http.GetFromJsonAsync<List<JsonComponent>>("/data.json");
foreach (var item in jsonComponentList)
{
JsonComponent componentItem = new();
componentItem.Type = Type.GetType($"{nameSpaceComponents}{item.Type}");
componentItem.Parameters = new();
foreach (var kvp in item.Parameters)
{
var jsonElement = ((JsonElement)kvp.Value).GetString();
if (kvp.Key.ToString().StartsWith("On"))
{
callback = EventCallBack.Factory.Create<string>(this, (Action<string>) this.GetType().GetMethod(kvp.Value.ToString()).CreateDelegate(typeof(Action<string>), this));
componentItem.Parameters.Add(kvp.Key, callback);
}
else
{
componentItem.Parameters.Add(kvp.Key, jsonElement);
}
}
components.Add(componentItem);
}
}
}

Blazor, MatBlazor - How to catch the value change of MatSelect component

I have used the MatBlazor framework for my project.
In MatSelect, I want to catch its value onchange event to do some other works.
I have tried some solutions but the onchange event has not fired yet.
<MatSelect Label="Customer" Value="#customer" ValueChanged="OnChangeCustomer">
<MatOptionString Value="-1">All</MatOptionString>
#foreach (var item in customers)
{
<MatOption Value="#item.Id">#item.Name</MatOption>
}
</MatSelect>
The below is my onchange event handler. But it did not fired when select another value in drop down list:
public void OnChangeCustomer(ChangeEventArgs args)
{
if (args.Value.ToString() != "-1")
isAccountDropDownListDisabled = false;
}
Can anyone help me?
Thanks
You could refer the following sample to using the MatSelect control:
<MatSelect Outlined="true" Label="Category" ValueChanged="(string i) => OnChangeCategory(i)">
<MatOptionString Value="-1">All</MatOptionString>
#foreach (var cat in GetCategories())
{
<MatOptionString Value="#cat.Id.ToString()">#cat.Name</MatOptionString>
}
</MatSelect>
<span>#selectedValue</span>
#code
{
public string selectedValue;
protected List<Customer> GetCategories()
{
//return new List<string>() { "AA", "BB" };
return new List<Customer>() {
new Customer(){Id=1001, Name="Tom"},
new Customer(){Id=1002, Name="David"},
new Customer(){Id=1003, Name="Lucy"}
};
}
protected void OnChangeCategory(string value)
{
//do something
selectedValue = "Selected Value: " + value;
}
}
The screenshot as below:
More detail information, check the MatSelect document.
The code from #ZhiLv works well but if you want a pre filled dynamic select value it will become harder.
I spent so many hours trying to get this to work with MatSelectValue with no luck.
https://www.matblazor.com/SelectValue
I ended up using a simple MatSelect with a property calling my onchange event method. This is the only way I got the select list to prefill correctly.
Example with nullable int but you can change to string, guid etc as well.
https://www.matblazor.com/Select#MatSelectGuid
#inject StateContainer StateContainer
<MatSelect Label="Choose threat" #bind-Value="#SelectThreatId" Outlined="true" FullWidth="true">
#foreach (var item in selectThreats)
{
<MatOption TValue="int?" Value="#item.Id">#item.Threat</MatOption>
}
</MatSelect>
#code
{
[Parameter]
public ThreatAndCountermeasureDto ThreatAndCountermeasureDto { get; set; }
List<ThreatDto> selectThreats = new List<ThreatDto>();
ThreatDto selectedThreat = null;
private int? threatId = null;
public int? SelectThreatId
{
get { return threatId; }
set
{
threatId = value;
SelectThreatValueChanged(value);
}
}
private void SelectThreatValueChanged(int? id)
{
selectedThreat = StateContainer.Threats.Single(x => x.Id == id);
}
protected override void OnInitialized()
{
base.OnInitialized();
StateContainer.OnChange += StateHasChanged;
SelectThreatId = ThreatAndCountermeasureDto.Threat.Id;
selectThreats = StateContainer.Threats.ToList();
}
...
Source:
https://github.com/SamProf/MatBlazor/issues/498

dynamically generating controls

I have a foreach loop and I need to create a button that allows the user to get the address of a specific location. The issue is when the page is generated, if you click ANY button, they all display the popover.
#foreach (var schedule in _schedules) {
<BSButton Id="popover1" onclick="onclick1">#schedule.Location.NickName</BSButton>
<BSPopover Target="popover1" IsOpen="#IsOpen1" Placement="Placement.Top">
<BSPopoverHeader>#schedule.Location.Name</BSPopoverHeader>
<BSPopoverBody>#schedule.Location.Address</BSPopoverBody>
</BSPopover>
}
code on top of the same page
#code {
bool IsOpen1 { get; set; }
void onclick1(MouseEventArgs e)
{
IsOpen1 = !IsOpen1;
StateHasChanged();
}
}
I am having trouble figuring out how to generate this type of control. I know the problem is the onclick is the same for all the controls. Even if I dynamically change the name in the onclick="#popoverTextId", how do I dynamically create the code in the #code {}
I'll assum Schedule has an Id. Otherwise, improvise something.
<BSButton #onclick="() => onclick1(schedule.Id)"> ... </BSButton>
.... IsOpen="#(schedule.Id == SelectedId)" ...
void onclick1(int scheduleId)
{
SelectedId = scheduleId;
//StateHasChanged();
}

Using RenderPartial to assign Model Property values

I am new to MVC platform and trying a simple application to get hold of the framework,
Application:I am designing an admin application which has a form to enter question and multiple options to the database, I have a viewmodel called "QuestionViewModel" which has properties "Quesiton" and a List of OptionsViewModel in it, "OptionViewModel" consists of "Option" and "IsRightAnswer" properties, so on the UI, I have a QuestionView which displays a textbox for Question and I want a question to have 4 options, so I created a PartialView called "QuestionOptionView" which takes "OptionViewModel" as its model,
now I loop about 4 times through the PartialView and display a TextBox for "Option" and "IsRightAsnwer" radiobutton,
The UI displays TetsBox for "Question" and 4 other "TextBoxes" for entering the "Options",
but when I Post this form, the "Options" does not bind to the ViewModel
How can I achieve Model Binding from these partialview data to the main viewmodel??
public class QuestionViewModel
{
[Display(Name = "Enter a question")]
public string Question { get; set; }
public IList<QuestionOptionViewModel> Options { get; set; }
public int MaxOptions { get { return 4; } }
public QuestionViewModel()
{
Options = new List<QuestionOptionViewModel>();
}
}
public class QuestionOptionViewModel
{
public string Option { get; set; }
public bool IsRightAnswer { get; set; }
}
In the viewm I have as below,
#using (Html.BeginRouteForm("savequestion", new {}, FormMethod.Post)){
<div>
#Html.LabelFor(m => m.Question)
#Html.TextBoxFor(m => m.Question)
</div>
<p>Add options for the question</p>
for (int i = 0; i < Model.MaxOptions; i++)
{
{ Html.RenderPartial("QuestionOption", new Babbi_Test_admin.Models.QuestionOptionViewModel()); }
}
<input type="submit" value="Save" />
When the form is submitted I have my post method as,
[HttpPost]
public void SaveQuestion(QuestionViewModel viewModel)
{
}
My viewModel in the post has "Options" as null
This is happening cause you must be binding the QuestionViewModel with the Action of your Controller instead, you need to create a ViewModel that will have the QuestionViewModel OptionViewModel
Error
[HttpPost]
public void SaveQuestion(QuestionViewModel viewModel)
{
}
In the Post Method you are expecting QuestionViewModel then how can you expect to get the OptionviewModel
Another thing your are Renbdering Partial View for QuestionOptionViewModel which Contains Options and RIght Answer so in the loop you will get the Right Answer 4 thing which is incorrect.
Sample
public class QuestionViewModel
{
//Properties
}
public class OptionViewModel
{
//Properties
}
public class QuestionAndOptionViewmodel
{
// QuestionViewModel and OptionViewModel model object
//Constructors
//Methods
}
[HttpPost]
public void SaveQuestion(QuestionAndOptionViewmodel viewModel)
{
//Your Code Here;
}
Try changing your code to:
for (int i = 0; i < Model.MaxOptions; i++)
{
{ Html.RenderPartial("QuestionOption", Model.Options.Item(i)); }
}
Or even better:
foreach(option in Model.Options)
{
Html.RenderPartial("QuestionOption", option);
}
You will need to initialize the option values in your Question Model constructor

Categories