pass function as parameter to dynamically created component blazor - c#

I am trying to pass some parameters to a dynamically generated component
Parent
...
DynamicComponent = builder =>
{
Type moduleType = Type.GetType(ChildComponentName);
System.Diagnostics.Debug.WriteLine(moduleType);
if (moduleType != null)
{
builder.OpenComponent(0, moduleType);
builder.AddAttribute(1, "title", "Delete + " + item.Name + "?");
builder.AddAttribute(2, "content", "Are you sure you want to delete this organization?");
builder.AddAttribute(3, "YesCallback", whatgoeshere?);
builder.AddComponentReferenceCapture(1, inst => { child = Convert.ChangeType(inst, moduleType); });
builder.CloseComponent();
}
};
public void Delete(string msg)
{
System.Diagnostics.Debug.WriteLine(msg);
items.RemoveAt(DeleteIndex);
}
...
Child
...
[Parameter] public string Title { get; set; }
[Parameter] public string Content { get; set; }
[Parameter] public EventCallback<string> YesCallback { get; set; }
...
The component get's generated and displayed fine. The first two parameter are set fine. What I can't figure out is how to pass the parent Delete function as third attribute so the child can call it.

You can use the EventCallbackFactory.Create to create event callbacks from simple actions. Along with the action you have to pass the event receiver which is usually the component on which the action is being executed.
You can access the factory through EventCallback.Factory:
var callback = EventCallback.Factory.Create<string>(this, arg =>
{
// do something
});
You can also pass a method with the correct signature instead:
var callback = EventCallback.Factory.Create<string>(this, OnCallback);
You can then pass the created event callback directly to the AddAttribute method:
builder.OpenComponent(0, componentType);
builder.AddAttribute(1, "OnSomething", callback);
builder.CloseComponent();

Here is what I got
[Parameter]
public EventCallback<MouseEventArgs> Click { get; set; }
public async Task OnClickCallback(MouseEventArgs e)
{
await Click.InvokeAsync(e);
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
var seq = 0;
builder.OpenElement(seq++, "li");
builder.AddAttribute(seq++, "onclick", EventCallback.Factory.Create<MouseEventArgs>(this, OnClickCallback));
builder.CloseElement();
}

Related

refer to components that created by loop

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.

Using parent to call a method from the components Blazor

I have this method in my components called ClearFiles(string PathName) which I currently call by clicking one of the buttons on the component, but now I'm trying to call of them at once but I cannot figure out how to do this. Calling a single ClearFiles method from the component works perfectly, but I'm not quite sure how to call all these methods at once using the parent class. I have made a button in the parent class with the name of ForceClean which I'm hoping can be pressed to call all of the components clear files function.
I create the components in the parent class using a for loop in my PageFileCleaner.razor file like this:
#foreach (var d in filters)
{
<CompFileFilter FileFilter="d" OnDelete="Delete" ></CompFileFilter>
}
Where filters is a List of type FileFilter which is in the namespace Common.DB. This is how it is declared in the parent class:
private List<FileFilter> filters { get; set; }
And is populated with data from my database using:
using var context = _db.CreateDbContext();
filters = await context.FileFilters.AsNoTracking().ToListAsync();
foreach( var filter in filters)
{
FileFilter f = new FileFilter();
}
And this Parameter (in the child class) is what i use to get the values from that row in the database using FileFilter.Pathname or FileFilter.ID for example:
[Parameter]
public FileFilter FileFilter { get; set; }
Database / FileFilter object class:
namespace Common.DB
{
public class FileFilter
{
[Key]
public int ID { get; set; }
public string TimerChoice { get; set; }
public bool IsLoading;
public string PathName { get; set; } = "";
public string Extension { get; set; }
//public string[] extensionList { get; set; }
...
}
}
The clear files function is in my component/child CompFileFilter.razor file (took the code out for this example because its over a hundred lines of code). I'm creating the components in the parent class and each one will have this ClearFiles() method.
public async Task ClearFiles(string PathName)
{
filtering code...
}
How can I loop through my components from the parent class and call all of there ClearFiles() method?
You can use a dictionary and store the reference of each CompFileFilter component. Then use the references to call the ClearFiles method. You can use the ID property of FileFilter for the dictionary keys.
PageFileCleaner.razor
#foreach (var d in filters)
{
<CompFileFilter #key="d.ID" #ref="_fileFilterRefs[d.ID]" FileFilter="d" OnDelete="Delete"></CompFileFilter>
}
#code {
private Dictionary<int, CompFileFilter> _fileFilterRefs = new Dictionary<int, CompFileFilter>();
private async Task ForceClean()
{
// Execute ClearFiles one at a time.
foreach (var item in _fileFilterRefs)
{
await item.Value.ClearFiles();
}
// Or execute them in parallel.
//var tasks = _fileFilterRefs.Select(item => Task.Run(item.Value.ClearFiles));
//await Task.WhenAll(tasks);
}
}
ClearFiles method doesn't need PathName parameter. You can get the PathName from the FileFilter parameter.
CompFileFilter.razor
#code {
[Parameter]
public FileFilter FileFilter { get; set; }
public async Task ClearFiles()
{
var pathName = FileFilter.PathName;
// filtering code...
}
}
Edit:
You should manually update the _fileFilterRefs dictionary whenever you remove an item from filters list to keep them synchronized. E.g.
private void Delete(FileFilter fileFilter)
{
_filters.Remove(fileFilter);
_fileFilterRefs.Remove(fileFilter.ID);
}
You should clear the _fileFilterRefs dictionary whenever you set the filters list to a new list. E.g.
private async Task RefreshFilters()
{
_fileFilterRefs.Clear();
_filters = await filterService.GetFileFilters();
}
But you don't have to do the same thing when you add a new item to the filters list (_filters.Add(...)). In that case it's handled automatically.
In summary:
filters.Add(new FileFilter()) -> do nothing.
filters.Remove(fileFilter) -> remove fileFilter.ID key from
_fileFilterRefs
filters = new List() -> clear _fileFilterRefs before setting the list
https://github.com/dotnet/aspnetcore/issues/17361#issuecomment-558138782

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);
}
}
}

Setting private set fields from a post request on backend server

I am making my way through various todo list tutorials while learning react and entity framework. As some background I have made my way though Microsoft's todo list todo tutorial; although I have replaced the front end part of that with my own front end. It was all working fine, until I've tried to extend it and hit the issue I will outline below.
I have updated the EF model to include private set fields for the added benefits (becoming read only after it is initialised etc). This is shown in the code below.
public class TodoItem
{
public long id { get; private set; }
public string title { get; private set; }
public bool IsComplete { get; private set; }
// Define constructor
public TodoItem(long newId, string newTitle)
{
id = newId;
title = newTitle;
IsComplete = false;
}
public void ToggleComplete()
{
IsComplete = !IsComplete;
}
}
The post action from the controller is shown below. I have included some debug printouts as these show where the field is already showing the title as null.
I believe this is the section of code I am struggling with and would like to know what mistakes I am making or what the best practices are!
[HttpPost]
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem item)
{
// returns null if model field set to private
System.Diagnostics.Debug.WriteLine("item title: " + item.title);
// Create new item passing in arguments for constructor
TodoItem newItem = new TodoItem(item.id, item.title);
_context.TodoItems.Add(newItem);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetTodoItem), new { id = newItem.id }, newItem);
}
The frontend method (js) where the post request is made is shown below:
const addTodoMethod = (title) => {
// Create new item
const item = {
title: title,
id: Date.now(),
isComplete: false,
}
// Update state
const newTodos = [...todos, item];
setTodos(newTodos);
// Can use POST requiest to add to db
axios.post('https://localhost:44371/api/todo/',
item)
.then(res=> {
console.log("Added item. Title: ", title);
})
.catch(function (error) {
console.log(error);
})}
I hope I've explained the problem well enough. Let me know if there is anything else needed!
I have updated the EF model to include private set fields for the added benefits (becoming read only after it is initialised etc).
There are two problems in what you did. The first one is that the Models must have a parameter-less constructor, and the second one that the properties must be public, both getter and setter.
The best you can do right now is to stop using your database entity for user input and create a ViewModel class:
public class TodoItemViewModel
{
public long id { get; set; }
public string title { get; set; }
public bool IsComplete { get; set; }
}
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItemViewModel model)
{
var item = new TodoItem(item.id, item.title);
...
}

Accessing list in adapter

Currently I am using C# with Xamarain and am developing for android. What I have right now is an activity (activity_MainView2) which holds a listview and has an adapter attached (adapter_MainView2). I also have a list (gCartList) which is inside of the activity, that I want to be able to access inside of the adapter to add the row id that the user clicks on. I have the onClick listeners set up inside of the adapter, but I'm not sure how to access the list. Sorry I'm pretty new to c#. Thank you in advance for the help.
From Activity
public List<Cart_List> gCartList { get; set; } = new List<Cart_List>();
When the cart button is press in the activity
public override bool OnOptionsItemSelected(IMenuItem item)
{
if (gDrawerToggle.OnOptionsItemSelected(item))
{
return true;
}
else
{
switch (item.ItemId)
{
case Resource.Id.cartButton:
Toast.MakeText(this, ("Cart Pressed").ToString(), ToastLength.Short).Show();
foreach (var tI in gCartList.OrderBy(a => a.Busn_ID)) System.Diagnostics.Debug.Write(tI);
return true;
}
}
return base.OnOptionsItemSelected(item);
}
In the Adapter
activity_MainView2 act = new activity_MainView2();
var things = act.gCartList;
things.Add(new Cart_List() { Busn_ID = "1", Item_ID = gItems[position].ID, Item_Qty = "1", Item_Name = gItems[position].Name, Item_Desc = gItems[position].Description, Item_Price = gItems[position].Price });
You can create a property in your activity and then it can be accessed like this:
public class Activity1
{
pubic Activity1()
{
this.SomeThings = new List<object>();
}
public List<object> SomeThings { get; set; }
}
public class Activity2
{
public Activity2()
{
// Now in this clas you can access the item to be shared
Activity1 act = new Activity1();
var things = act.SomeThings;
}
}
Or you can pass it to the constructor like this:
public class Activity2
{
private List<object> somethings;
public Activity2(List<object> somethings)
{
//
this.somethings = somethings;
}
}

Categories