How does ElementReference in Blazor work? - c#

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.

Related

Blazor update UI inside component foreach loop

I have Blazor components in a way similar to this:
Main.razor
#foreach (Vehicle vehicle in _vehicles)
{
if (vehicle.Visible)
{
<VehicleComponent Vehicle=#vehicle></VehicleComponent>
}
}
#code {
protected override void OnInitialized()
{
_vehicles = new List<Vehicles>();
// -> _vehicles being filled here
base.OnInitialized();
}
}
VehicleComponent.razor
#if (Vehicle != null)
{
<img src="#(Vehicle.src)"/>
<div id="#(Vehicle.Id)" tabindex="#(Vehicle.tabindex)">
<h3>#(Vehicle.text)</h3>
</div>
}
#code {
[Parameter] public Vehicle Vehicle { get; set; }
}
The problem is that all the VehicleComponents inside the loop are rendered after the loop is finished. But I want the component to completely render, before the next List item will be rendered.
I tried using StateHasChanged() after each item in Main.razor, but I got an infinite loop.
Is there a way I can render each component in the loop after each other (and update the UI)?
If you want the visual effect of 'gradually appearing' items:
protected override async Task OnInitializedAsync ()
{
var temp = new List<Vehicles>();
// -> _vehicles being filled here
_vehicles = new List<Vehicles>();
foreach (var vehicle in temp)
{
_vehicles.Add(vehicle);
StateHasChanged();
await task.Delay(500); // adjust the delay to taste
}
}
but be aware that this is an expensive (slow) way to show them.
Using #key won't improve this but it might help when you change the list later:
<VehicleComponent #key=vehicle Vehicle=#vehicle></VehicleComponent>
I think you are misunderstanding the Render process.
Your Razor code gets compiled into a C# class with your Razor compiled as a RenderFragment (a delegate) - you can see it in the project obj/Debug/Net5 folder structure. The component "Render" events queue this delegate onto the Renderer's Queue. The Renderer executes these delegates and applies changes to the Renderer's DOM. Any changes in the Renderer's DOM get passed to the browser to modify it's DOM.
Henk's answer adds the items slowly to the list. It adds a "Render" event through StateHasChanged and then yields through the Task.Delay to let the Renderer re-render the list on each iteration.

Get value of droplink in Code-Behind

I am trying to get the display name of an item in a droplink in the back-end C# code. I am using Sitecore 6.6, not using MVC, and am setting a droplink control in the CMS for clients called Address. The droplink source goes to /sitecore/Templates/User Defined/WAC/Address, and the individual items have an SEO-compliant name and a readable display name.
For example:
Item ID: {9E60F5F8-FBF2-4CBD-BB13-6A93397AAC87}
Name: 100-main-street
Display Name: 100 Main Street, Sample Town, 10011
My code:
protected void Page_Load(object sender, EventArgs e)
{
String sl = "";
Sitecore.Data.Items.Item currentItem = Sitecore.Context.Item;
// BEGIN main class list
Sitecore.Collections.ChildList classList = currentItem.Children;
foreach (Sitecore.Data.Items.Item mainPage in classList)
{
if (mainPage.TemplateID.ToString() == "{27A9692F-AE94-4507-8714-5BBBE1DB88FC}")
{
sl += "<span class=\"address\">" + mainPage.Fields["Address"] +"</span>";
}
else
{
}
}
// END main class list
classSessionList.Text = sl;
}
This code will give me the ID of the Item. If I use mainPage.Fields["Address"].DisplayName, I get "Address".
How can I get the Display Name of the item from the droplink?
Use LookupField for getting reference item below are the sample code:
LookupField address= (LookupField)mainPage.Fields["Address"];
Item addressItem = address.TargetItem;
string displayName = addressItem.Fields["DisplayName"].Value;
If you want it in one line then use below code:
((LookupField)mainPage.Fields["Address"]).TargetItem.DisplayName
Type cast the field to a ReferenceField. Then access the TargetItem property:
sl += "<span class=\"address\">" + ((ReferenceField)mainPage.Fields["Address"]).TargetItem.DisplayName +"</span>";
The thomas answer would work based on your code. But I would suggest you also to try to stick with ASP.Net and Sitecore server components.
That will avoid Null Reference errors, will support Page Editor better and also makes your code easier to maintain.
You can have a repeater in your markup like this:
<asp:Repeater ID="rptAdresses" runat="server">
<ItemTemplate>
<span class="address">
<sc:Text id="scAddress" runat="server" Field="__Display Name" Item="<%#(Sitecore.Data.Items.Item)Container.DataItem%>"></sc:Text>
</span>
</ItemTemplate>
</asp:Repeater>
And then bind the address on your code behind:
private void BindRepeater()
{
if (mainPage.TemplateID.ToString() != "{27A9692F-AE94-4507-8714-5BBBE1DB88FC}")
{
rptAdresses.Visible = false;
}
else
{
rptAdresses.DataSource = Sitecore.Context.Item.GetChildren();
rptAdresses.DataBind();
}
}
Another point that I notice was the line mainPage.TemplateID.ToString() != "{27A9692F-AE94-4507-8714-5BBBE1DB88FC}". That is a point that you also could improve. Hard coded IDs are not a good pratice. You could create a class to hold those things or, even better, you could think more about your design to avoid it.
cheers
All other solutions are perfect, but you can also use the code below:
var itemID=mainPage.Fields["Address"].value;
Item targetItem=Sitecore.Context.Database.GetItem(itemID);
if (mainPage.TemplateID.ToString() == "{27A9692F-AE94-4507-8714-5BBBE1DB88FC}")
{
sl += "<span class=\"address\">" + targetItem.DisplayName +"</span>";
}
else
{
}
With this approach you can use every field of target item.

Win 8.1 SearchBox SuggestionsRequested

I got an userControl that contains a searchBox.
This UserControl is inside another one.
I got a strange behavior while i'm searching, because the suggestionCollection works in a strange way.
Example :
in the searchBox i write something all works perfectly, if i choose the item it also works.
But if i try to use backspace (after the choose) i got no suggestion.
I cannot understand why it doesn't work.
That's the code
//var deferral = args.Request.GetDeferral(); //it seems to not influence the behavior
var suggestionCollection = args.Request.SearchSuggestionCollection;
try
{
TransporterExt tr_search = new TransporterExt();
//queryText is a string inserted in the searchBox
if (string.IsNullOrEmpty(queryText)) return;
tr_search.name = queryText;
suggested.Clear(); //that's a collection..
//just a search that return a collection of objects TransporterExt
querySuggestions = await TransporterService.Search_StartsWith(tr_search);
if (querySuggestions.Count > 0)
{
int i = 0;
foreach (TransporterExt tr in querySuggestions)
{
string name = tr.name;
string detail = tr.trId.ToString();
string tag = i.ToString();
string imageAlternate = "imgDesc";
suggestionCollection.AppendResultSuggestion(name, detail, tag, imgRef, imageAlternate);
this.suggested.Add(tr);
i++;
}
}
}
catch (System.ArgumentException exc)
{
//Ignore any exceptions that occur trying to find search suggestions.
Debug.WriteLine(exc.Message);
Debug.WriteLine(exc.StackTrace);
}
//deferralComplete(); //it seems to not influence the behavior
The problem is that: all the variables have the right value, but the suggestion panel appears only if i make a particular search: it appears when i change the first letter of search, or after an wrong seach
What appends when i make a search
What appends if i use the backspace, and i what i want to fix
As i said, all works perfectly, after the "backspace" action the suggestionCollection got the right value...but the panel is missing.
Could someone help me?
You can use SearchBox and SuggestionRequested event to fire the event when type on the SearchBox. I will show an Example
<SearchBox x:Name="SearchBoxSuggestions" SuggestionsRequested="SearchBoxEventsSuggestionsRequested"/>
and write the SearchBoxEventsSuggestionsRequested handler in the code behind
private void SearchBoxEventsSuggestionsRequested(object sender, SearchBoxSuggestionsRequestedEventArgs e)
{
string queryText = e.QueryText;
if (!string.IsNullOrEmpty(queryText))
{
Windows.ApplicationModel.Search.SearchSuggestionCollection suggestionCollection = e.Request.SearchSuggestionCollection;
foreach (string suggestion in SuggestionList)
{
if (suggestion.StartsWith(queryText, StringComparison.CurrentCultureIgnoreCase))
{
suggestionCollection.AppendQuerySuggestion(suggestion);
}
}
}
}
You can add the keyword to SuggestioList, and it will show in the dropdown when you type on the Searchbox.
Create the SuggestionList
public List<string> SuggestionList { get; set; }
initialize the list
SuggestionList = new List<string>();
and add keywords to the list
SuggestionList.Add("suggestion1");
SuggestionList.Add("suggestion2");
SuggestionList.Add("suggestion3");
SuggestionList.Add("suggestion4");
SuggestionList.Add("Fruits");
Thanks.

Filter ListBox with TextBox in realtime

I am trying to filter an listbox with text from a textbox, realTime.
Here is the code:
private void SrchBox_TextChanged_1(object sender, EventArgs e)
{
var registrationsList = registrationListBox.Items.Cast<String>().ToList();
registrationListBox.BeginUpdate();
registrationListBox.Items.Clear();
foreach (string str in registrationsList)
{
if (str.Contains(SrchBox.Text))
{
registrationListBox.Items.Add(str);
}
}
registrationListBox.EndUpdate();
}
Here are the issues:
When I run the program i get this error: Object reference not set to an instance of an object
If I hit backspace, my initial list is not shown anymore. This is because my actual list of items is now reduced, but how can I achieve this?
Can you point me in the right direction?
It's hard to deduct just from the code, but I presume your filtering problem born from the different aspects:
a) You need a Model of the data shown on ListBox. You need a colleciton of "Items" which you hold somewhere (Dictionary, DataBase, XML, BinaryFile, Collection), some kind of Store in short.
To show the data on UI you always pick the data from that Store, filter it and put it on UI.
b) After the first point your filtering code can look like this (a pseudocode)
var registrationsList = DataStore.ToList(); //return original data from Store
registrationListBox.BeginUpdate();
registrationListBox.Items.Clear();
if(!string.IsNullOrEmpty(SrchBox.Text))
{
foreach (string str in registrationsList)
{
if (str.Contains(SrchBox.Text))
{
registrationListBox.Items.Add(str);
}
}
}
else
registrationListBox.Items.AddRange(registrationsList); //there is no any filter string, so add all data we have in Store
registrationListBox.EndUpdate();
Hope this helps.
Something like this might work for you:
var itemList = registrationListBox.Items.Cast<string>().ToList();
if (itemList.Count > 0)
{
//clear the items from the list
registrationListBox.Items.Clear();
//filter the items and add them to the list
registrationListBox.Items.AddRange(
itemList.Where(i => i.Contains(SrchBox.Text)).ToArray());
}
Yes that was the answer to filtering. (modified a bit). I had the info in a text file. This is what worked for me
FileInfo registrationsText = new FileInfo(#"name_temp.txt");
StreamReader registrationsSR = registrationsText.OpenText();
var registrationsList = registrationListBox.Items.Cast<string>().ToList();
registrationListBox.BeginUpdate();
registrationListBox.Items.Clear();
if (!string.IsNullOrEmpty(SrchBox.Text))
{
foreach (string str in registrationsList)
{
if (str.Contains(SrchBox.Text))
{
registrationListBox.Items.Add(str);
}
}
}
else
while (!registrationsSR.EndOfStream)
{
registrationListBox.Items.Add(registrationsSR.ReadLine());
}
registrationListBox.EndUpdate();
It seems that the error:
Object reference not set to an instance of an object
is from somewhere else in my code, can't put my finger on it.
If able, store everything in a dictionary and just populate it from there.
public partial class myForm : Form
{
private Dictionary<string, string> myDictionary = new Dictionary<string, string>();
//constructor. populates the items. Assumes there is a listbox (myListbox) and a textbox (myTextbox), named respectively
public myForm()
{
InitializeComponent();
myDictionary.Add("key1", "item1");
myDictionary.Add("key2", "My Item");
myDictionary.Add("key3", "A Thing");
//populate the listbox with everything in the dictionary
foreach (string s in myDictionary.Values)
myListbox.Add(s);
}
//make sure to connect this to the textbox change event
private void myTextBox_TextChanged(object sender, EventArgs e)
{
myListbox.BeginUpdate();
myListbox.Items.Clear();
foreach (string s in myDictionary.Values)
{
if (s.Contains(myListbox.Text))
myListbox.Items.Add(s);
}
myListbox.EndUpdate();
}
}
I would do it like this:
private List<string> registrationsList;
private void SrchBox_TextChanged_1(object sender, EventArgs e)
{
registrationListBox.BeginUpdate();
registrationListBox.Items.Clear();
var filteredList = registrationList.Where(rl => rl.Contains(SrchBox.Text))
registrationListBox.Items.AddRange();
registrationListBox.EndUpdate();
}
Just remember to populate registrationsList the first time you fill your listbox.
Hope this helps.
it was a very hard issue for me, but I found a workaround (not so simple) that works fine for me.
on aspx page:
<input id="ss" type="text" oninput="writeFilterValue()"/>
<asp:HiddenField ID="hf1" runat="server" Value="" ClientIDMode="Static" />
I need HTML input type because of "oninput" function, that is not availiable on classic asp.net controls. The writeFilterValue() function causes a postback that filters values of a given ListBox (in code-behind).
I've defined this two javascript function:
<script type="text/javascript">
function writeFilterValue() {
var bla = document.getElementById("ss").value;
$("#hf1").val(bla)
__doPostBack();
}
function setTboxValue(s) {
document.getElementById('ss').value = s;
document.getElementById('ss').focus();
}
</script>
You can now use postback on code-behind to capture hf1 value, every time some single Character is typed on inputbox.
On code-behind:
If IsPostBack Then
FiltraLbox(hf1.Value)
End If
The function FiltraLbox(hf1.Value) changes datasource of Listbox, and rebind it:
Public Sub FiltraLbox(ByVal hf As String)
If hf <> "" Then
' change datasource here, that depends on hf value,
ListBox1.DataBind()
ScriptManager.RegisterStartupScript(Page, Page.GetType(), "text", setTboxValue('" + hf + "');", True)
End If
End Sub
At the end I call the function setTboxValue(), that rewrites the input text value lost on postback, and puts the focus on it.
Enjoy it.

Selenium2 Webdriver C# .Click() List - Stale Reference Exception

I need some help because I keep getting a StaleElementReference when I try to parse a list of a tags to click.
What I have done is on page land I iterate through the page and generate an object List<> with with all the a tags
private List<IWebElement> _pageLinks;
public List<IWebElement> pageLinks
{
get
{
if (_pageLinks == null)
{
_pageLinks = InfoDriver.FindElements(By.TagName("a")).ToList();
}
return _pageLinks;
}
}
Then I want to parse this list, and click each one and then go back to the page it was referenced from.
private static SeleniumInformation si = new SeleniumInformation(ffDriver);
si.pageLinks.ForEach(i =>
{
i.Click();
System.Threading.Thread.Sleep(1000);
ffDriver.Navigate().Back();
});
What happens is that after the first click it goes to the new page and then goes back to the starting page but it can't get the next link. I've tried setting it to a static element, setting a backing field so that it checks to see if there is data there already however it appears that on click the IwebElement looses the list and it doesn't rebuild the list either so I get a StaleElementReference exception not handled and element not found in cache.
Is this a bug in Selenium with the IWebElement class or am I doing something wrong? Any help would be greatly appreciated.
This is the expected behavior. You left the page the element was on. When you navigated back, it is a new page and that element is no longer on it.
To work around this I would suggest passing around Bys instead, if you can. Assuming your anchorlinks all have unique hrefs, you could instead generate a list as follows (java code, but should translate to c#):
private static List<By> getLinks(WebDriver driver)
{
List<By> anchorLinkBys = new ArrayList<By>();
List<WebElement> elements = driver.findElements(By.tagName("a"));
for(WebElement e : elements)
{
anchorLinkBys.add(By.cssSelector("a[href=\"" + e.getAttribute("href") + "\"]"));
//could also use another attribute such as id.
}
return anchorLinkBys;
}
I don't know the makeup of your page so I don't know if it is possible to generate By's dynamically that uniquely identify the elements you want. For example if all the elements have the same parent, you could use the css level 3 selector nth-child(n). Hopefully you get some ideas from the above code.
private void YourTest()
{
IWebDriver browserDriver = new FirefoxDriver();
browserDriver.Navigate().GoToUrl(pageUrl);
int linkCount= browserDriver.FindElements(By.TagName("a")).Count;
for (int i = 0; i <= linkCount-1; i++ )
{
List<IWebElement> linksToClick = browserDriver.FindElements(By.TagName("a")).ToList();
linksToClick[i].Click();
System.Threading.Thread.Sleep(4000);
if(some boolean check)
{
//Do something here for validation
}
browserDriver.Navigate().Back();
}
broswerDriver.Quit();
}

Categories