Blazor: How to use a component from 2 different pages with 2 different states - dependency-injection

In a Blazor application, I want to use a component from two different pages. The component uses a service to maintain its state. But each page needs to have the component use a different state. Here's how I thought I would do it (using the default Counter component to demonstrate).
CounterService.cs
namespace TestTwoInterfaceToOneService.Shared
{
public interface ICounterServiceA
{
int CurrentCount { get; set; }
void IncrementCount();
}
public interface ICounterServiceB
{
int CurrentCount { get; set; }
void IncrementCount();
}
public class CounterService : ICounterServiceA, ICounterServiceB
{
public int CurrentCount { get; set; }
public void IncrementCount()
{
CurrentCount++;
}
}
}
In Program.cs add:
builder.Services.AddScoped<ICounterServiceA, CounterService>();
builder.Services.AddScoped<ICounterServiceB, CounterService>();
Counter.razor
<h1>Counter</h1>
<p>Current count: #CounterService.CurrentCount</p>
<button class="btn btn-primary" #onclick="IncrementCount">Click me</button>
#code {
[Parameter]
public CounterService CounterService { get; set; }
private void IncrementCount()
{
CounterService.CurrentCount++;
}
}
PageA.razor
#page "/pagea"
#inject ICounterServiceA CounterServiceA
<h3>Page A</h3>
<Counter CounterService="CounterServiceA" /> #*<-- Error: Cannot convert from ICounterServiceB to CounterService*#
PageB.razor
#page "/pageb"
#inject ICounterServiceB CounterServiceB
<h3>Page B</h3>
<Counter CounterService="CounterServiceB" /> #*<-- Error: Cannot convert from ICounterServiceB to CounterService*#
I get the error 'Cannot convert from ICounterServiceB to CounterService' when I try to pass the service to the component. I've discovered that using 2 identical interfaces pointing to the same concrete implementation does indeed give me 2 scoped instances. However, I cannot figure out how to pass those instances into the component.
Is there a piece that I'm missing? Or, should this be done some other way?
SOLUTION
Using a combination of the answer from Henk and the comments from Brian, the solution I came up with is:
CounterService.cs
namespace TestTwoInterfaceToOneService.Shared
{
public class CounterService
{
public int CurrentCount { get; set; }
public void IncrementCount()
{
CurrentCount++;
}
public void ResetCount()
{
CurrentCount = 0;
}
}
}
CounterStateService.cs
using System.Collections.Generic;
namespace TestTwoInterfaceToOneService.Shared
{
public interface ICounterStateService
{
CounterService this[string key] { get; }
}
public class CounterStateService : ICounterStateService
{
private Dictionary<string, CounterService> counterServices = new();
public CounterService this[string key]
{
get
{
if (!counterServices.ContainsKey(key))
{
counterServices.Add(key, new CounterService());
}
return counterServices[key];
}
}
}
}
In Program.cs, add
builder.Services.AddScoped<ICounterStateService, CounterStateService>();
Counter.razor
<p>Current count: #CounterService.CurrentCount</p>
<button class="btn btn-primary" #onclick="IncrementCount">Increment</button>
#code {
[Parameter]
public CounterService CounterService { get; set; }
private void IncrementCount()
{
CounterService.CurrentCount++;
}
}
PageA.razor
#page "/pagea"
#inject ICounterStateService CounterStateService
<h3>Page A</h3>
<Counter CounterService='CounterStateService["A"]' />
<button class="btn btn-primary" #onclick='CounterStateService["A"].ResetCount'>Reset Count</button>
PageB.razor
#page "/pageb"
#inject ICounterStateService CounterStateService
<h3>Page B</h3>
<Counter CounterService='CounterStateService["B"]' />
<button class="btn btn-primary" #onclick='CounterStateService["B"].ResetCount'>Reset Count</button>

Or, should this be done some other way?
Yes. Baking this usage pattern into the Type structure isn't going to adapt or scale well. What if you need a third page, or want to make it dynamic?
You could use a wrapper service and a scheme with a key to bind a State to a Component:
public class CounterService { ... } // no need to register
public class CounterStateService // register for injection
{
private Dictionary <string, CounterService> stateLookup = new();
public CounterService this[string key]
{
if (! stateLookup.tryGetValue (key, out CounterService service))
{
service = new CounterService();
stateLookup.Add(key, service);
}
return service;
}
}
and use it like
#page "/pagea"
#inject CounterStateService CounterStateService
<h3>Page A</h3>
<Counter CounterService="CounterStateService["A"]" />

Related

How to retrive data from dynamic text box using Blazor

MyCustomControl.razor
<input type="text" id="#id" />
#code {
[Parameter]
public string id { get; set; }
}
Test.Razor
#page "/test"
<button #onclick="#addCompoment">add text box</button>
<div class="simple-list-list">
#if (componentListTest == null)
{
<p>You have no items in your list</p>
}
else
{
<ul>
#foreach (var item in componentListTest)
{
#item<br/>
}
</ul>
}
</div>
#functions {
private List<RenderFragment> componentListTest { get; set; }
private int currentCount { get; set; }
private string TxtExample { get; set; }
protected void OnInit()
{
currentCount = 0;
componentListTest = new List<RenderFragment>();
}
protected void addCompoment()
{
if(componentListTest==null)
{
componentListTest = new List<RenderFragment>();
}
componentListTest.Add(CreateDynamicComponent(currentCount));
currentCount++;
}
RenderFragment CreateDynamicComponent(int counter) => builder =>
{
try
{
var seq = 0;
builder.OpenComponent(seq, typeof(MyCustomControl));
builder.AddAttribute(++seq, "id", "listed-" + counter);
builder.CloseComponent();
}
catch (Exception ex)
{
throw;
}
};
}
After Adding the textbox dynamically,how to retrieve all input data from the textbox (after clicking on the submit button.)
How to interact with dynamic component and fetch Value.
MyCustomControl is component, Append in Test Razor Page.
for these component create an attribute like bind-value to get input field data given by user
There are a couple of solutions to this type of issue, depending on the general design of your app, constraints, and such like. The following solution is simple. Generally speaking, it involves passing the value of the added text box to a parent component to be saved in a list object. The parent component has a button that displays the list of text when clicked.
The following is the definition of the child component:
MyCustomControl.razor
<input type="text" #bind="#Value" id="#ID" />
#code {
private string _value;
public string Value
{
get { return _value; }
set
{
if (_value != value)
{
_value = value;
if (SetValue.HasDelegate)
{
SetValue.InvokeAsync(value);
}
}
}
}
[Parameter]
public string ID { get; set; }
[Parameter]
public EventCallback<string> SetValue { get; set; }
}
Usage in a parent component
<button #onclick="#addCompoment">add text box</button>
<div class="simple-list-list">
#if (componentListTest == null)
{
<p>You have no items in your list</p>
}
else
{
<ul>
#foreach (var item in componentListTest)
{
#item
<br />
}
</ul>
}
</div>
<p><button #onclick="#ShowValues">Show values</button></p>
#if (Display)
{
<ul>
#foreach (var value in values)
{
<li>#value</li>
}
</ul>
}
#code {
public void SetValue(string value)
{
values.Add(value);
}
private List<RenderFragment> componentListTest { get; set; }
private List<string> values = new List<string>();
private int currentCount { get; set; }
protected override void OnInitialized()
{
currentCount = 0;
componentListTest = new List<RenderFragment>();
}
private bool Display;
private void ShowValues()
{
if (values.Any())
{
Display = true;
}
}
protected void addCompoment()
{
if (componentListTest == null)
{
componentListTest = new List<RenderFragment>();
}
componentListTest.Add(CreateDynamicComponent(currentCount));
currentCount++;
}
RenderFragment CreateDynamicComponent(int counter) => builder =>
{
try
{
builder.OpenComponent(0, typeof(MyCustomControl));
builder.AddAttribute(1, "id", "listed-" + counter);
builder.AddAttribute(2, "SetValue", Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.TypeCheck<Microsoft.AspNetCore.Components.EventCallback<System.String>>(Microsoft.AspNetCore.Components.EventCallback.Factory.Create<System.String>(this, this.SetValue )));
builder.CloseComponent();
}
catch (Exception ex)
{
throw;
}
};
}
Note:
Notice the SetValue attribute I've added to the CreateDynamicComponent's builder. This provides a Component Parameter to MyCustomControl of type EventCallback<string> which is assigned to the SetValue parameter property:
[Parameter]
public EventCallback<string> SetValue { get; set; }
And it is used (trigger the method which is also called SetValue in the parent component. You can change the name if you like) to pass the changed value from the child component to the parent component.
Use code instead of functions.
Note that I've made some modifications in your code: OnInitialized instead of OnInit (obsolete), sequence numbers should not created the way you do. Refer to this article written by Steve Sanderson ...
Hope this helps...

What exactly triggers a Blazor component to redraw itself?

I have Blazor WebAssembly application. I have a parent and child component that share a model class. When the child updates a value on the model, the child redraws itself, but the parent does not. So what exactly triggers a Blazor component to redraw itself? I can't find any documentation on this particular aspect of Blazor.
I have an POCO model class. In the real application this comes from a separate assembly. Models/AwesomenessModel.cs:
namespace BlazorApp.Models {
public class AwesomenessModel {
public int? Level { get; set; }
}
}
My index page. Pages/Index.razor:
#page "/"
#using BlazorApp.Components
#using BlazorApp.Models
<Awesomeness AwesomenessModel="#AwesomenessModel"></Awesomeness>
#code {
private AwesomenessModel AwesomenessModel = new AwesomenessModel();
}
The parent class. I have added a redraw button to verify that the content should have changed. Components/Awesomeness.razor:
#using BlazorApp.Models
#if (AwesomenessModel.Level.HasValue) {
<div>Awesomeness is #AwesomenessModel.Level.Value</div>
} else {
for (int i = 0; i < 10; i++) {
<AwesomenessSelector AwesomenessModel="#AwesomenessModel" Level="#i"></AwesomenessSelector>
}
}
<div><button class="btn btn-link" #onclick="RedrawClick">Redraw</button></div>
#code {
[Parameter]
public AwesomenessModel AwesomenessModel { get; set; }
private void RedrawClick() {
this.StateHasChanged();
}
}
The child component that does the actual change to the Awesomeness model. Components/AwesomenessSelector.razor:
#using BlazorApp.Models
#if (!AwesomenessModel.Level.HasValue) {
<div><button class="btn btn-link" #onclick="LevelClick">#Level</button></div>
}
#code {
[Parameter]
public AwesomenessModel AwesomenessModel { get; set; }
[Parameter]
public int Level { get; set; }
private void LevelClick() {
AwesomenessModel.Level = Level;
}
}
When I run the application and click on an awesomeness level, that level disappears (the AwesomenessSelector component redraws), but the rest are still visible and there is no 'Awesomeness is...' message (the Awesomeness component does not update). If I then click the Redraw button to force the Awesomeness component to redraw, I get the correct 'Awesomeness is...' message.
I'm probably not structuring the code in the correct way to get the parent component to redraw automatically. How should I structure it in this scenario?
Child Component add a EventCallback for OnLevelClick
#using BlazorApp.Models
#if (!AwesomenessModel.Level.HasValue) {
<div><button class="btn btn-link" #onclick="LevelClick">#Level</button></div>
}
#code {
[Parameter]
public AwesomenessModel AwesomenessModel { get; set; }
[Parameter]
public int Level { get; set; }
[Parameter]
public EventCallback<string> OnLevelClick { get; set; }
private void LevelClick() {
AwesomenessModel.Level = Level;
OnLevelClick.InvokeAsync();
}
}
parent Component add Handler for OnLevelClick
#using BlazorApp.Models
#if (AwesomenessModel.Level.HasValue) {
<div>Awesomeness is #AwesomenessModel.Level.Value</div>
} else {
for (int i = 0; i < 10; i++) {
<AwesomenessSelector AwesomenessModel="#AwesomenessModel" Level="#i" #OnLevelClick="OnLevelClick"></AwesomenessSelector>
}
}
#code {
[Parameter]
public AwesomenessModel AwesomenessModel { get; set; }
private void OnLevelClick() {
this.StateHasChanged();
}
}
hope this helps
You need StateHasChanged() on the parent control that doesn't know that the child has been updated. I believe you should create an event on the child that is triggered when you need to update the parent. Then on the parent when the event is triggered StateHasChanged(). All theory but should work.

conditional DI with the help of config file

How can we achieve conditional Dependency Injection with the help of Unity Application block config file ? Below is my piece of code.
namespace DependencyInjection
{
//UI
class Program
{
static void Main(string[] args)
{
IUnityContainer objContainer = new UnityContainer();
objContainer.LoadConfiguration(); //loads from app
Customer obj = objContainer.Resolve<Customer>();
obj.CustomerName = "test1";
obj.Add();
}
}
//ML
public class Customer
{
private IDAL Odal;
public string CustomerName { get; set; }
public Customer(IDAL iobj)
{
Odal = iobj;
}
public void Add()
{
Odal.Add();
}
}
//DAL
public interface IDAL
{
void Add();
}
public class SQLServerDAL:IDAL
{
public void Add()
{
Console.WriteLine("SQL Server inserted");
Console.ReadLine();
}
}
public class OracleDAL:IDAL
{
public void Add()
{
Console.WriteLine("Oracle inserted");
Console.ReadLine();
}
}
}
And my config file is like below:
<unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
<container>
<register type="DependencyInjection.IDAL, DependencyInjection"
mapTo="DependencyInjection.SQLServerDAL,DependencyInjection"/>
</container>
</unity>
How can I achieve the following :
If(somecondition=true)
Customer object should resolve to SQLServerDal
Else
Customer object should resolve to OracleDal
Is it possible ? If yes, how ?

How to access DropDown values in Struts 2 Action

My requirement is, at beginning I want to show users data on page and when user make changes in form, I want to access changed data.
Below is my code in Action class,
public class DisplayData extends ActionSupport implements ModelDriven<List<User>>, Preparable {
private List<User> userList;
#Override
public void prepare() throws Exception {
userList = new ArrayList<User>();
userList.add(new User("Demo","N"));
userList.add(new User("Demo1","Y"));
userList.add(new User("Demo2","Y"));
userList.add(new User("Demo3","N"));
}
#Override
public List<User> getModel() {
return userList;
}
public String execute(){
for (User value: userList) {
System.out.println(value.getName() +":"+value.getFlag());
}
return "success";
}
public List<User> getUserList() {
return userList;
}
public void setUserList(List<User> userList) {
this.userList = userList;
}
}
User class,
public class User implements Serializable
{
private String name;
private String flag;
public User() {}
public User(String name,String flag) {
super();
this.name = name;
this.flag = flag;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getFlag() {
return flag;
}
public void setFlag(String flag) {
this.flag = flag;
}
}
Code in Jsp page,
<s:form name="getData" action="getData" method="post">
<table>
<s:iterator value="model" status="rowStatus">
<tr>
<td>
<s:textfield name="model[%{#rowStatus.index}].name" value="%{model[#rowStatus.index].name}"/>
<s:select name="%{model[#rowStatus.index].flag}" value="%{model[#rowStatus.index].flag}"
list="#{'Y':'Yes','N':'No'}" />
</td>
</tr>
</s:iterator>
</table>
<s:submit name="ok" value="ok" />
</s:form>
When page get rendered, it shows appropriate value of textfield and dropdown.
If I changed the values in Textfield and droprdown and submit the form then I am getting modified value of textfield but for the dropdwon it shows old value. How can I access selected value of dropdown?
Got the answer... :)
It was syntax mistake. Instead of
<s:select name="%{model[#rowStatus.index].flag}" value="%{model[#rowStatus.index].flag}"
list="#{'Y':'Yes','N':'No'}" />
Use
<s:select name="model[#rowStatus.index].flag" value="%{model[#rowStatus.index].flag}"
list="#{'Y':'Yes','N':'No'}" />
I have used %{ } in name attribute..

Problem Binding via command.Can you help

New to wpf and through a learning curve.
I have a userControl with a Toolbar Save Button and a TextBox.
What I am trying to achieve is as follows
When I press the save Button in the toolbar I should record in the textbox that I am about to save and that I have saved the customer (CustomerView UserControl)
I seem to have 2 problems
1) that the SaveCommand is not hooked I thought I had hooked it
2) is not writing the action to the textbox.
Could you tell me where I am going wrong?
Thanks a lot!!!
MainWindow.xaml
<Window x:Class="MyCompany.CustomerStore.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:view="clr-namespace:MyCompany.CustomerStore.Views"
Title="MainWindow" Height="350" Width="525">
<Grid>
<view:CustomerView></view:CustomerView>
</Grid>
CustomerView.xaml
<UserControl x:Class="MyCompany.CustomerStore.Views.CustomerView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<DockPanel LastChildFill="True">
<ToolBar DockPanel.Dock="Top">
<Button Command="{Binding Path=SaveCommand}">Save</Button>
</ToolBar>
<TextBox Name="txtPrintAction" Text="{Binding CustomerLog, Mode=TwoWay}"></TextBox>
</DockPanel>
</Grid>
CustomerModel.cs
public class CustomerModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string CustomerLog { get; set; }
}
CustomerViewModel.cs
public class CustomerViewModel:WorkspaceViewModel,ICustomerViewModel
{
readonly CustomerModel _customerModel;
RelayCommand _saveCommand;
public CustomerViewModel(CustomerModel customer)
{
_customerModel = customer;
}
public string FirstName
{
get { return _customerModel.FirstName; }
set
{
_customerModel.FirstName = value;
base.OnPropertyChanged("FirstName");
}
}
public string LastName
{
get { return _customerModel.LastName; }
set
{
_customerModel.LastName = value;
base.OnPropertyChanged("LastName");
}
}
public string CustomerLog
{
get { return _customerModel.CustomerLog; }
set
{
_customerModel.CustomerLog = value;
base.OnPropertyChanged("CustomerLog");
}
}
public ICommand SaveCommand
{
get
{
if (_saveCommand == null)
{
_saveCommand = new RelayCommand(param => Save(), param => CanSave);
}
return _saveCommand;
}
}
private void Save()
{
AppendToLog("I am about to save");
//Pretend we have saved the customer
AppendToLog("CustomerSaved");
}
internal void AppendToLog(string text)
{
_customerModel.CustomerLog += text + Environment.NewLine; ;
OnPropertyChanged("CustomerLog");
}
static bool CanSave
{
get
{
return true;
}
}
Where do you declare a relationship between the view
x:Class="MyCompany.CustomerStore.Views.CustomerView
and the model class CustomerViewModel?
I don't see that anywhere.
I think you need to set the DataContext of the View to the Model.

Resources