Just thinking about coming up with a templated blazor component to do a CRUD style Single Page App which can have a specific object passed in so I don't have to write the same boilerplate code over and again.
so for instance as below parts of it can be templated by using RenderFragment objects:
#typeparam TItem
<div>
#if (AddObjectTemplate != null)
{
#AddObjectTemplate
}
else
{
<div style="float:left">
<button class="btn btn-primary" #onclick="AddObject">Add Object</button>
</div>
}
</div>
#code {
[Parameter]
public RenderFragment AddObjectTemplate { get; set; }
[Parameter]
public IList<TItem> Items { get; set; }
}
However further down I might want to have something like this:
<button class="btn btn-default" #onclick="#(() => EditObject(item.Id))">Edit</button>
protected void EditObject(int id)
{
TItem cust = _itemServices.Details(id);
}
The issue is that the above call to EditObject(item.Id) cannot resolve to a specific object at this moment because it does not know what TItem is. Is there a way to use a specific interface in the template component that each object must implement or is there another way of doing this?
The idea would be to have AddObject, EditObject, DeleteObject etc which all basically do the same thing but with different types of object.
Since you have the IList<TItem> as a parameter, the list exists at another level of the component structure outside of this component. Because of this you might be better off using the EventCallBack<T> properties for your Add, Edit, and Delete methods, and having the actual methods set as you wire the component up. This makes your template component a rendering object only, and you keep the real "work" to be done close to the actual list that needs the work done.
When you set up your template component, you might try something like this which I've had good results with.
Templator.razor
#typeparam TItem
<h3>Templator</h3>
#foreach (var item in Items)
{
#ItemTemplate(item)
<button #onclick="#(() => EditItemCallBack.InvokeAsync(item))">Edit Item</button>
}
#code {
[Parameter]
public IList<TItem> Items { get; set; }
[Parameter]
public EventCallback<TItem> EditItemCallBack { get; set; }
[Parameter]
public RenderFragment<TItem> ItemTemplate
}
Container.Razor
<h3>Container</h3>
<Templator TItem="Customer" Items="Customers" EditItemCallBack="#EditCustomer">
<ItemTemplate Context="Cust">
<div>#Cust.Name</div>
</ItemTemplate>
</Templator>
#code {
public List<Customer> Customers { get; set; }
void EditCustomer(Customer customer)
{
var customerId = customer.Id;
//Do something here to update the customer
}
}
Customer.cs
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
}
The key points here would be as follows:
The actual root list is living outside the templated component, as are the methods to work on that list, so all of the above are at the same level of abstraction and the same chunk of code.
The template component receives a list, a type to specify what type of the list items will be, and a method callback to execute on each item. (or series of methods, you can add the "Add" and "Delete" methods as you see fit using the same approach). It also receives the <ItemTemplate> render fragment that you specify when calling the code in the Container.razor file.
The 'foreach' in the templated item makes sure each TItem gets set up for it's own RenderFragment, set of buttons and callback functions.
Using the EventCallBack<TItem> as a parameter means that you assign a method to it that expects back the whole object of TItem. This is good, as the template now doesn't care what type the TItem is, only that is has the ability to call a method that takes a TItem as an argument! Handling the instance of whatever TItem is is now the responsibility of the calling code, and you don't have to try to constrain the generic type TItem. (Which I haven't had any luck doing in Blazor yet, maybe future releases)
As for how to render whatever TItem you feed into it, that is explained well in the documentation HERE.
Hope this helps!
Related
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>
I have an IList<IControl> that is an interface that derives from IComponent, every implementation of the interface inherits from ComponentBase. The component is instantiated from a factory dynamically (it returns a component compatible with input's type).
Now I want to render this list, but I do not know how, this is my ControlsContainer.razor:
#foreach (var control in OrderedControls)
{
<div #key=#Guid.NewGuid().ToString()>
#RenderWidget(control)
</div>
}
I want to avoid a switch/if-else if with every component type (they are loaded dynamically with reflection, I do not need to register them somewhere).
Untested, but the following should work, or at least put you on the right path ...
#foreach (var control in OrderedControls)
{
<div #key=#Guid.NewGuid().ToString()>
#RenderWidget(control)
</div>
}
#code {
RenderFragment RenderWidget(IControl control)
{
var concreteType = control.GetType();
RenderFragment frag = new RenderFragment(b =>
{
b.OpenComponent(1, concreteType);
b.CloseComponent();
});
return frag;
}
}
I would like to use a two-way databinding between a component inside a template and its parents. The one way data binding is working well. But when I modified the Context passed from the RenderFragment this modification is not propagate to the template (container). Here is the example.
This it the template definition. We have a form and we want to be able to specify the content of the form in function of the model.
#typeparam TItem
#typeparam TItemDtoCU
#typeparam TDataService
<EditForm Model="#Item" OnValidSubmit="HandleValidSubmit" class="item-editor">
<DataAnnotationsValidator />
#FormContentTemplate(Item)
<span class="save">
<MatButton Type="submit" Raised="true">Save</MatButton>
<MatButton Type="reset" Raised="true" OnClick="HandleCancelSubmit">Cancel</MatButton>
</span>
</EditForm>
Here is the place where I use this template
<ModelEditorTemplate TItem="ClassName"
TItemDtoCU="OtherClassName"
TDataService="ServiceClassName"
ItemId="SelectedItem?.Id"
OnItemAdded="ItemAdded"
OnItemUpdated="ItemUpdated">
<FormContentTemplate>
<span>
<MatTextField Label="Name" #bind-Value="context.Name" #bind-Value:event="onchange" />
<ValidationMessage For="#(() => context.Name)" />
</span>
</FormContentTemplate>
</ModelEditorTemplate>
When the user modified the MatTextField field is reset to the initial value provided by the template component.
Do you have an idea ?
Edit 1 : More information about the way we fetched the data
Yes the TDataService is fetching the component from a REST Api. Here the partial class linked to the template :
public partial class ModelEditorTemplate<TItem, TItemDtoCU, TDataService> : ParentThatInheritsFromComponentBase
where TItem : BaseEntity, new()
where TDataService : ICrudService<TItem, TItemDtoCU>
{
public TItem Item = new TItem();
[Inject]
protected TDataService DataService { get; set; }
protected override async Task OnParametersSetAsync()
{
//...
await LoadItem();
// ...
}
protected async Task LoadItem()
{
//...
Item = await DataService.Get(ItemId.Value);
// ....
}
}
When the field inside FormContentTemplate RenderFragment update the model, it triggers the OnParametersSetAsync method of the template ,which fetch again the data from the server, update the model and then overwrite the modification done by the user.
There was no issue, with the form input inside the template just bad understanding of how it works.
I corrected the problem by using the SetParametersAsync to check if the parameter that change was the one, I was looking for and not the RenderFragment. The ItemId is the id of the record, I fetch from the DataService.
public override async Task SetParametersAsync(ParameterView parameters)
{
_hasIdParameterChange = false;
if(parameters.TryGetValue(nameof(ItemId), out int? newItemIdValue))
{
_hasIdParameterChange = (newItemIdValue != ItemId);
}
await base.SetParametersAsync(parameters);
}
I currently have a model which contains an object property that can either be an int, a string or a MyClass object (which contains 3-4 properties) in runtime. Here is the general idea of my model:
public class MyModel
{
// Can be int, string, or MyClass during runtime.
public object Value { get; set; }
}
Here is the general idea of MyClass:
public class MyClass
{
// These are correctly displayed in the EditorFor template.
public int Quantity { get; set; }
public string Name { get; set; }
public bool IsAvailable { get; set; }
}
In my view, I have an EditorFor(m => m.Value) that correctly selects a template based on its runtime type.
During POST, the data from the view is correctly bound to the model and sent back to the controller as a parameter, but only if it's a string or an int. In fact, when Value is a MyClass object, it seems to be completely empty as if it came right out of a "= new Object();" statement. I cannot inspect it, as seen below:
I suspect this behavior is because on POST, the Value object is recreated using an empty constructor without having its runtime type specified, so when the binding happens, the properties don't match with a plain System.Object object.
If this is the case, how can I specify the object's runtime type during its re-creation? If not, how can I fix this binding issue?
Extra info: The MyClass properties are accessible through Request during controller POST (for example, Request["MyModel.Quantity"] would return the proper int value from the view). They just can't seem to get in the model.
Edit: As requested, here is what my view looks like:
#model MyModel
<div class="section">
<div class="editor">
<div class="editor-title">The Value field</div>
<div class="editor-container">
#Html.EditorFor(m => m.Value)
</div>
</div>
</div>
I have a CreateViewModel.
public class CreateViewModel
{
public AttributesViewModel AttributesInfo { get; set; }
}
The AttributesViewModel is sent to a partial view.
public class AttributesViewModel
{
public AttributesViewModel()
{
ChosenAttributes = new List<int>();
}
public List<Attributes> Attributes { get; set; }
public List<int> ChosenAttributes { get; set; }
}
The List of Attributes is outputted in the partial view. Each one has a checkbox.
foreach (var attribute in Model.Attributes)
{
<input type="checkbox" name="ChosenAttributes" value="#attribute.ID" /> #Attribute.Name
}
When I post CreateViewModel, AttributesInfo.ChosenAttributes is always empty even though I checked some boxes. How do I properly name each checkbox so that it binds to the ChosenAttributes List?
My Solution
I took Stephen Muecke's suggestion to do the two way binding. So, I created a CheckboxInfo class that contained Value, Text, and IsChecked. I created a EditorTemplate for it:
#model Project.CheckboxInfo
#Html.HiddenFor(model => model.Text)
#Html.HiddenFor(model => model.Value)
#Html.CheckBoxFor(model => model.IsChecked) #Model.Text
One GIANT caveat. To get this to work properly, I had to create an EditorTemplate for the AttributesViewModel class. Without it, when CreateViewModel is posted, it cannot link the checkboxes to AttributesInfo.
Your naming the checkbox name="ChosenAttributes" but CreateViewModel does not contain a property named ChosenAttributes (only one named AttributesInfo). You may be able make this work using
<input type="checkbox" name="AttributesInfo.ChosenAttributes" value="#attribute.ID" /> #Attribute.Name
but the correct approach is to use a proper view model that would contain a boolean property (say) bool IsSelected and use strongly typed helpers to bind to your properties in a for loop or using a custom EditorTemplate so that your controls are correctly names and you get 2-way model binding.
I had a similar scenario, but this was how I did it. The solution is not perfect so please excuse if I have left something out, but you should be able to relate. I tried to simplify your solution as well :)
I changed the Attribute class name to CustomerAttribute, rename it to whatever you like, use a singular name, not plural. Add a property to your CustomerAttribute class, call it whatever you like, I called mine IsChange.
public class CustomerAttribute
{
public bool IsChange { get; set; }
// The rest stays the same as what you have it in your Attributes class
public string Name { get; set; } // I'm assuming you have a name property
}
Delete your AttributesViewModel class, you don't really need it, I like simplicity.
Modify your CreateViewModel class to look like this:
public class CreateViewModel
{
public CreateViewModel()
{
CustomerAttributes = new List<CustomerAttribute>();
}
public List<CustomerAttribute> CustomerAttributes { get; set; }
}
Your controller will look something like this:
public ActionResult Create()
{
CreateViewModel model = new CreateViewModel();
// Populate your customer attributes
return View(model);
}
Your post controller action method would look something like this:
[HttpPost]
public ActionResult Create(CreateViewModel model)
{
// Do whatever you need to do
}
In your view, you will have something like this:
<table>
<tbody>
#for (int i = 0; i < Model.CustomerAttributes.Count(); i++)
{
<tr>
<td>#Html.DisplayFor(x => x.CustomerAttributes[i].Name)</td>
<td>#Html.CheckBoxFor(x => x.CustomerAttributes[i].IsChange)</td>
</tr>
}
<tbody>
</table>
Create a sample app and try out the code above and see if it works for you.