I'm working on a side project. One of the design decisions is that components shouldn't do any database calling (if this is wrong or impractical then I'm open to disregarding this design policy). I'm also using radzen component library, but has no bearing on this.
I have a button component that is basically two buttons. one for save (a sub part of an object), and a dropdown/context button to save all (all aspects of a more complex object). this button component is a component and can be used in multiple places. so my idea is to pass the save methods to the button from a parent page. but what's happening in practice is that the save component is a child in another component. this is no big deal, the page defines the save methods, passes it to the component which then passes to the child. where things get hairy is that the method definition is not really definable on the button. sometimes I'm passing a func<Task>
other times i'm passing func<myType,Task>
. so to account for this is I'm setting the method as a dynamic and calling invoke on that. Here's how that looks.
@inject ContextMenuService ContextMenuService
<RadzenStack Gap="0px" Orientation="Orientation.Horizontal">
<RadzenButton Text=@SaveText Click="SaveAction" class="rz-border-radius-0" ButtonStyle="ButtonStyle" />
<RadzenButton Icon="expand_more" class="rz-border-radius-0" ButtonStyle="ButtonStyle" Click="@(args=>ShowDropdownMenu(args))" Visible=hasContextItems />
</RadzenStack>
@code{
[Parameter]
public Func<Task> SaveAction { get; set; }
[Parameter]
public List<ContextMenuItem> ContextMenuList { get; set; } = new List<ContextMenuItem>();
[Parameter]
public ButtonStyle ButtonStyle { get; set; }
[Parameter]
public string SaveText { get; set; }
private bool hasContextItems => ContextMenuList.Count > 0;
void ShowDropdownMenu(MouseEventArgs args)
{
ContextMenuService.Open(args, ContextMenuList, OnMenuItemClick);
}
async void OnMenuItemClick(MenuItemEventArgs args)
{
dynamic method = args.Value;
await method.Invoke();
ContextMenuService.Close();
}
}
usage looks like this:
...
<RadzenColumn>
<ContextSaveComponent SaveText="Save Board" ButtonStyle="ButtonStyle.Success" ContextMenuList="saveContextMenu"
SaveAction="(async()=>await SaveBoardChanges(Model, Model.Boards))" />
</RadzenColumn>
...
@code{
[Parameter]
public Func<PricingModel, List<Board>, Task> SaveBoardChanges { get; set; }
private List<ContextMenuItem> saveContextMenu = new List<ContextMenuItem>();
protected override async Task OnInitializedAsync()
{
saveContextMenu.Add(new ContextMenuItem
{
Text = "Save All Pricing Model Changes",
Value = (async()=> await SaveAllPricingModelChanges(Model))
});
}
}
in the snippet above, the SaveBoardChanges
is a parameter and is a child component of a page. The page defines these methods and passes them in like so:
@page "/myPage"
<RadzenTabs RenderMode="TabRenderMode.Client" TabPosition="TabPosition.Left" u/bind-SelectedIndex=@selectedPricingModelIndex>
<Tabs>
@foreach(var model in pricingModels)
{
int i = pricingModels.IndexOf(model);
<RadzenTabsItem [email protected]>
<PricingModelComponent Model=@pricingModels[i]
SaveAllPricingModelChanges="SavePricingModelAsync"
SaveBoardChanges="SaveBoardChangesAsync"
/>
</RadzenTabsItem>
}
</Tabs>
</RadzenTabs>
@code{
private async Task SavePricingModelAsync(PricingModel model)
{
await DataSource.UpdatePricingModelAsync(model);
}
private async Task SaveBoardChangesAsync(PricingModel model, List<Board> boards)
{
model.Boards = boards;
await DataSource.UpdatePricingModelAsync(model);
}
}
This all works, but having to use the dynamic type to circumvent strong typing generates a code smell from a (potentially bad) design policy caused by not wanting components to make out of process calls (database updates), and only having pages define methods that make that make out of process calls. Is my design policy fine and this is just one of the things I need to do, or should I abandon the policy (on this early project)?
dynamic
toFunc<Task>
, you could also use generics to do a method extension so it can be reused across project. \$\endgroup\$Func<Task>
but are sometimesFunc<MyType, Task>
so that's why it's a dynamic and not a particular Func implementation. \$\endgroup\$if(method is Func<Task> func)
andif(method is Func<MyType, Task> func)
or you could just use reflection to cover allFunc<,>
. \$\endgroup\$Invoke
method exists, but at this point is it meaningful, or worth the cost of having to reflect all the different types ofFunc<,>
with my custom types as well. this code review was mainly to ask if my policy of "no out of process calls in a component, only on pages" was okay if it forced me to write code like this. \$\endgroup\$