Trouble getting StateHasChanged to update an #if Statement - c#

So I'm having trouble getting my Razor page to update an if statement.
The idea is when a user selects a button, it'll recount the string looking for how many of X values there are, right now we are looking for spaces. If theres one space, I want the input field locked (the user has no reason edit it). If theres more than one space, its unlocked.
Below is the if statement holding the tags
#if (NumOfSelectedValue <= 1)
{
<input class="form-control-sm" value="1" style="width: 40px" disabled />
}
else if (NumOfSelectedValue > 1)
{
<input class="form-control-sm" value="#NumOfSelectedValue" style="width: 40px" />
}
And here is the logic on how I thought it would be updated.
public void SpaceSelected() //ive used "async task"
{
int NumOfSelectedValue = SelectedCell.Count(x => x == ' ');//counting how many spaces there are with Linq
Console.WriteLine(NumOfSelectedValue);//post num of spaces in the console
//other versions ive used
//StateHasChanged();//update the if statement
//await InvokeAsync(StateHasChanged);
InvokeAsync(StateHasChanged);
}

Per your comments, you have a public property named NumOfSelectedValue. This is separate from the local variable NumOfSelectedValue you defined in SpaceSelected. Your solution is to not declare that local variable and instead just update the property:
public void SpaceSelected()
{
NumOfSelectedValue = SelectedCell.Count(x => x == ' ');
StateHasChanged(); // Probably not necessary unless you're calling this method in the background
}
Notice that we no longer have the int part in front of the assignment, which is what was declaring the variable.

Related

How do I access the code block within a Blazor RenderFragment?

I'm new to programming, C# Blazor, and RenderFragments in general, so forgive me if this is a basic question.
I need to set up a tabbed dialog interface where different tabs appear based on which menu buttons are clicked. I set up a DevExpress menu item which appears as a button:
<DxMenuItem Text="States" IconCssClass="homeMenuButtonStates" CssClass="menuItemSm" Click="OnStatesBrowseClick" />
When the user clicks it, the OnStatesBrowseClick function is called:
public void OnStatesBrowseClick()
{
tabName[tdiIndex] = "States";
TdiNewTab(tabName[tdiIndex]);
}
As you can see, its job is to assign a value to a member of a string array (tdiIndex was assigned "0"), and call the TdiNewTab method with argument tabName[tdiIndex] which evaluates to "States."
TdiNewTab has a switch statement which, for now, only has a case for "States."
public void TdiNewTab(string switchTabName) {
switch (switchTabName)
{
case "States":
renderStatesBrowsePage(tdiIndex);
break;
}
tdiIndex++;
}
In the event that "States" comes up, it points to renderStatesBrowsePage which should create a tab.
private RenderFragment renderStatesBrowsePage(int index)
{
deleteMe++;
RenderFragment item = tabsChildContent =>
{
deleteMe++;
tabsChildContent.OpenComponent<DxTabPage>(0);
tabsChildContent.AddAttribute(2, "Text", $"Tab # {index}");
tabsChildContent.AddAttribute(3, "ChildContent", (RenderFragment)((tabPageChildContent) =>
{
tabPageChildContent.OpenComponent<States>(0);
tabPageChildContent.CloseComponent();
}));
tabsChildContent.CloseComponent();
};
return item;
}
However, nothing happens. Using breakpoints, I determined that the block within "RenderFragment item = tabsChildContent =>" does not get executed. Indeed, when I put both "deleteMe++"s in there, only the first one incremented the variable.
I'm at a loss as to why the block won't execute. Any advice would be much appreciated!
In response to answer from Mister Magoo:
I'm now trying to call the RenderFragment from within my markup:
<DxMenuItem Text="States" IconCssClass="homeMenuButtonStates" CssClass="menuItemSm" Click="StatesBrowseClick">
#if (statesBrowse == 1)
{
#renderStatesBrowsePage(0);
}
</DxMenuItem>
#code
public void StatesBrowseClick()
{
int statesBrowse = 1;
}
However, nothing still happens. I made sure statesBrowse, which was initialized to "0" outside of the method, equaled 1 after the click. I also tried removing the #if statement (leaving just "#renderStatesBrowsePage(0);", and I got the same result.
Note: I'm using VS 2019, and in StatesBrowseClick, it tells me that "The variable 'statesBrowse' is assigned, but its value is never used." This seems inconsequential, though, as I have tried manually setting it to "1" and removing the "if" statement.
The block won't execute because there is nothing executing it.
renderStatesBrowsePage returns a RenderFragment that needs to be rendered - but you are discarding it here:
public void TdiNewTab(string switchTabName) {
switch (switchTabName)
{
case "States":
// This returns a RenderFragment but you are discarding it
renderStatesBrowsePage(tdiIndex);
break;
}
tdiIndex++;
}
It is hard to tell you what you should be doing without seeing more of your project, but generally a RenderFragment is rendered by adding it to the component markup
<div>some stuff</div>
<div>
#MyRenderFragment
</div>

Is there a way to trigger two consecutive events in Blazor with a single click?

Background
I have created the card game called SET in Blazor WASM. In this game you click 3 cards and this can result in either a succesful SET, or a false submission. It works well and now I want to add an additional functionality to signal the outcome of the submission.
Desired result
First method: Upon clicking the 3rd card, the 3 cards should get a green (correct) or red (false) background and after x amount of time (say 1 second) a second method should fire.
Second method: If set was correct, replace the 3 cards with 3 new ones. If set was false, reset background color to white and reset border color to black.
Actual current result
What currently happens is that the result of the first method (green/red background) doesn't show, because there is only one callbackevent. So it does get executed, but it won't become visible until the second method is also executed, by which time either the cards have been replaced, or the backgroundcolor/bordercolor have been reset.
Tried so far
I tried to separate the two methods within the #onclick event, but there is still only one eventcallback and I could not find another way to do this so far, nor on stack overflow.
#onclick="() => { ProcessSelection(uniqueCardCombinations[index]); ProcessSetReplacement(); }
The most notable difference is that EventCallback is a single-cast
event handler, whereas .NET events are multi-cast. Blazor
EventCallback is meant to be assigned a single value and can only
call back a single method. https://blazor-university.com/components/component-events/
I also tried the "State Container" as described here by Chris Sainty, but that only re-renders it and I couldn't adjust it to my situation (not sure that's even possible tbh):
https://chrissainty.com/3-ways-to-communicate-between-components-in-blazor/
Code
I do intend to clean up the code once I get it working, making it more descriptive and splitting up the ProcessSetReplacement a bit more, but wanted to make it work first.
If you need more background/code info either let me know or you can find the entire repository here: https://github.com/John-Experimental/GamesInBlazor/tree/21_AddSetValidationVisualisation
SetPage.razor:
<div class="cardsContainer">
#for (int i = 0; i < numberOfCardsVisible; i++)
{
var index = i;
<div class="card #lineClass" style="background-color:#uniqueCardCombinations[index].BackGroundColor;
border-color:#uniqueCardCombinations[index].BorderColor;"
#onclick="() => { ProcessSelection(uniqueCardCombinations[index]); ProcessSetReplacement(); }">
#for (int j = 0; j < uniqueCardCombinations[i].Count; j++)
{
<div class="#uniqueCardCombinations[i].Shape #uniqueCardCombinations[i].Color #uniqueCardCombinations[i].Border"></div>
}
</div>
}
</div>
Relevant parts of SetPage.razor.cs (code behind)
private void ProcessSelection(SetCardUiModel setCard)
{
numberOfSelected += _uiHelperService.ProcessCardSelection(setCard);
if (numberOfSelected == 3)
{
var setSubmission = uniqueCardCombinations.Where(card => card.BackGroundColor == "yellow").ToList();
var potentialSet = _mapper.Map<List<SetCardUiModel>, List<SetCard>>(setSubmission);
var isSet = _cardHelperService.VerifySet(potentialSet);
_uiHelperService.SignalSetSubmissionOutcome(setSubmission, isSet);
};
}
private void ProcessSetReplacement()
{
// If it wasn't a set submission, you do nothing
if (numberOfSelected == 3)
{
var redBorderedCards = uniqueCardCombinations.Where(card => card.BorderColor == "red").ToList();
var countGreenBorders = uniqueCardCombinations.Count(card => card.BorderColor == "green");
// The while ensures that the 'ProcessSelection' function, which is also called, has run first
while (redBorderedCards.Count == 0 && countGreenBorders == 0)
{
Thread.Sleep(125);
redBorderedCards = uniqueCardCombinations.Where(card => card.BorderColor == "red").ToList();
countGreenBorders = uniqueCardCombinations.Count(card => card.BorderColor == "green");
}
// Wait 1.5 seconds so that the user can see the set outcome from 'ProcessSelection' before removing it
Thread.Sleep(1500);
if (countGreenBorders == 3)
{
// Replace the set by removing the set submission entirely from the list
uniqueCardCombinations.RemoveAll(card => card.BackGroundColor == "yellow");
numberOfSelected = 0;
// Check if the field currently shows more cards than normal (can happen if there was no set previously)
// If there are more cards, then remove 3 cards again to bring it back down to 'normal'
numberOfCardsVisible -= numberOfCardsVisible > settings.numberOfCardsVisible ? 3 : 0;
EnsureSetExistsOnField();
}
else
{
foreach (var card in redBorderedCards)
{
card.BackGroundColor = "white";
card.BorderColor = "black";
}
}
};
}
When you need to execute several timed actions in an event, async is your main tool.
Blazor will recognize async event handlers:
//private void ProcessSelection(SetCardUiModel setCard)
private async Task ProcessSelection(SetCardUiModel setCard)
{
... // old code
await Task.Delay(1000);
ProcessSetReplacement();
}
when you have more than one Task.Delay() then add StateHasChanged() calls before them.
And in the markup section:
#onclick="() => ProcessSelection(uniqueCardCombinations[index])"
Do not call both methods in onclick.
Call ProcessSelection, process result and set red/green there.
Call StateHasChanged()
Set timer to one second in ProcessSelection. Use Timer from System.Timers.
On Elapsed of the timer call ProcessSetReplacement.

How does ElementReference in Blazor work?

I'm building a dropdown list that filters based on input. When you click the down arrow it should focus on the first item in the list. The following is a simplified version of the issue I'm getting.
When you enter "a" in the input and then delete it, and afterwards press the down arrow, I get: The given key was not present in the dictionary. Can someone explain why this would happen? Why wouldn't the key be in the dictionary? What stopped it from being added?
<div class="parent" >
<div class="container">
<input class="input" #oninput="FilterIdentifiers" #onkeydown="(e) => HandleKBEvents(e)"/>
<div class="dropdown">
#{
int i = 1;
foreach (var identifier in identifiersUI)
{
<div class="dropdown-item" #key="identifier" tabindex="#i" #ref="refs[identifier]">#identifier</div>
i++;
}
}
</div>
</div>
#code {
private List<string> identifiers = new List<string>
{
"Aaa",
"Aab",
"Aba",
"Aca",
"Baa",
"Bba"
};
private List<string> identifiersUI = new List<string>();
private Dictionary<string, ElementReference> refs = new Dictionary<string, ElementReference>();
protected override async Task OnInitializedAsync()
{
identifiersUI = identifiers;
}
private void FilterIdentifiers(ChangeEventArgs e)
{
string input = e.Value.ToString();
refs.Clear();
identifiersUI = identifiers.Where(s => s.ToUpper().StartsWith(input.ToUpper())).ToList();
}
private void HandleKBEvents(KeyboardEventArgs e)
{
if (e.Code == "ArrowDown")
{
refs[identifiersUI[0]].FocusAsync();
}
}
}
When I change the FilterIdentifiers method to this it does work however. I saw this GitHub Blazor #ref and I thought I would try to only remove the keys that were filtered out to see if that made any difference...and it actually worked. Don't really understand why tho...
private void FilterIdentifiers(ChangeEventArgs e)
{
string input = e.Value.ToString();
var id = identifiers.Where(s => s.ToUpper().StartsWith(input.ToUpper())).ToList();
//remove removed elements from refs
var elements = identifiersUI.Except(id);
identifiersUI = id;
foreach (var element in elements)
{
refs.Remove(element);
}
}
Simply remove the refs.Clear() and let the Blazor UI manage refs.
private void FilterIdentifiers(ChangeEventArgs e)
{
string input = e.Value.ToString();
//refs.Clear();
identifiersUI = identifiers.Where(s => s.ToUpper().StartsWith(input.ToUpper())).ToList();
}
There's a lot going on under the hood when you use #ref with an HTML element rather than a Component object. When you clear refs manually, on the next render event, Blazor still thinks they're there, so doesn't add them again. Filtering out the a's removes the first value, hence the error. If you put break points in, you'll find you only have two elements in refs, the b's. Filter on b's and all appears to work, but in fact you only have four elements in refs - only the a's are present.
I would say this is due to the fact that the entire dictionary was cleared.
And I presume you were hoping for Blazor to "re build" that dictionary every time the identifiersUI list was reset, while looping over it to redraw.
But Blazor's change tracking probably decided (and rightfully so) that those elements hadn't changed and left them alone. And it's especially keeping track of your dropdown-items, taking special care not fiddle too much with those elements who's identifier has not changed, since you have set the #key attribute.

Dynamically change the Body for a component in Blazor

I'm trying to achive a search functionality for Blazor-server where the idea is to use it anytime on the site by typing on a search box which causes the page to change the #Body for a component that shows the results of the search.
Currently I'm able to do the search well on the MainLayout but this is by having already a list there and the Body component either below or on top. What I need is to only show the List AFTER I input something on the search bar and to replace it with the Body.
Here is what works but whithout the issue I am having.
<input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search" #oninput="(ChangeEventArgs e)=>SearchHandler(e)" />
<BrowseListShared #ref="BrowseListShared" />
#Body
code{
public string Searched { get; set; }
protected BrowseListShared BrowseListShared;
public void SearchHandler(ChangeEventArgs e)
{
Searched = e.Value.ToString();
BrowseListShared.UpdadeMe(Searched);
}
}
And this is my attempt at trying to make the replacement which gives me the error "Object reference not set to an instance of an object.", the Error shows when I type something in the search box.
<input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search" #oninput="(ChangeEventArgs e)=>SearchHandler(e)" />
#if (setVisible){
<BrowseListShared #ref="BrowseListShared" />
}else{
#Body
}
code{
public string Searched { get; set; }
protected BrowseListShared BrowseListShared;
private bool setVisible=false;
public void SearchHandler(ChangeEventArgs e)
{
if (e != null && e.Value.ToString() != ""){
setVisible = true;
}else{
setVisible=false;
}
Searched = e.Value.ToString();
BrowseListShared.UpdadeMe(Searched);
}
}
Hope someone can give me some direction to deal with this, thank you.
Edit:
Adding if(BrowseLit != null) to avoid error does make it work with some issues.
1- the first character makes so it shows just the list without the search because on the first character the code doesnt have the reference yet for the BrowseListShared so it skips the BrowseListShared.UpdateMe for the first tipe.
2- On deleting the text in the box completely until its blank and typing again will cause this error 'Cannot access a disposed object.'
There shouldn't be an issue to add a small if-block, the following is a basic concept that works for me:
<button #onclick="#( () => test = !test )">test</button>
#if (!test)
{
#Body
}
else
{
<span>some other search content - use a component here
and pass the data as a parameter to it, and its OnParametersSetAsync
can fetch needed data: #test</span>
}
#code{
bool test { get; set; }
}
I would also suggest you try using parameters for the search details instead of a reference.
If you want to show a particular page with search results, you can consider navigating the user to that page (e.g., pass the search query as a route parameter to it) - then it will render only what you want in the #Body - which can range from nothing, to search results, to a lot of other things.

Convert text to always be uppercase in Blazor

I am trying to make my input text always be uppercase in a blazor input text field. I tried creating a custom InputText like the below but it doesn't update the binding (appears as uppercase but doesn't bind as such). What am I doing wrong here? Is there a better way?
#inherits InputText
<input #attributes="AdditionalAttributes"
class="#CssClass"
value="#CurrentValue"
#oninput="EventCallback.Factory.CreateBinder<string>(
this, __value => CurrentValueAsString = __value.ToUpper(), CurrentValueAsString.ToUpper())" />
Simplicity works for me.
<input type="text" oninput="this.value=this.value.toUpperCase()" #bind=CurrentValueAsString />
For simplicity and (in my opinion) UX reasons, let's assume that the user is allowed to type lowercase letters, but the application will ToUpper the input after they leave that field.
First, make a new component so we can reuse the logic. I called mine UppercaseTextInput.razor
<input value="#UserInput"
#onchange="async e => await OnChange(e)" />
#code {
[Parameter]
public string UserInput { get; set; }
[Parameter]
public EventCallback<string> UserInputChanged { get; set; }
private async Task OnChange(ChangeEventArgs e)
{
var upperCase = (e.Value as string).ToUpperInvariant();
await UserInputChanged.InvokeAsync(upperCase);
}
}
The private method OnChange gets called whenever the input loses focus. Change #onchange to #oninput to make this happen every keystroke.
Now you can use this component in another page or component. For example, in the index page:
#page "/"
<h1>Hello, world!</h1>
<UppercaseTextInput #bind-UserInput="UserInput" />
<p>You typed: #UserInput</p>
#code {
public string UserInput { get; set; } = "Initial";
}
In this example, I have "Initial" as the starting text, which will be printed inside the text box. As soon as you leave the element, the text inside will be transformed to be uppercase.
The benefit of using a component is that you can do a standard #bind- with your properties.
I was using an old reference to the input which was causing the error. There was a bug in the code though (if the input was null). The corrected version can be seen here:
#inherits InputText
<input #attributes="AdditionalAttributes"
class="#CssClass"
value="#CurrentValue"
#oninput="EventCallback.Factory.CreateBinder<string>(
this, __value => CurrentValueAsString = __value?.ToUpper(), CurrentValueAsString?.ToUpper())" />
The answer to this was easier than all the answers: it can be solved using CSS text-transform: uppercase;. This is all you needed and it doesn't take any processing power because it's included in the browser's engine.
I'm using MudBlazor.
I scratched my head for a while why the heck adding
Style="text-transform: uppercase;"
to <MudInput> is not working, even with !important attribute the default user agent style (text-transform: none;) was applied.
C# or JS seemed overkill for the task in my opinion.
What finally worked for me was adding
input {
text-transform: uppercase;
}
to the site's CSS sheet (index.css in my case).
This eventually overwritten the default user agent style.

Categories