I have some text fields binding inside a for loop.
<Input type="text" #onchange='(ChangeEventArgs e)=>DataChange(e,item)' value="#item.value" />
here if user type anything to the input fields I have a custom validation inside DataChange function.
private void DataChange(ChangeEventArgs e, PnL pnl)
{
if(e.Value.ToString().Contains(10))
{
pnl.value = e.Value.ToString();
}
}
if change is valid then only Iam assigning value to the item in the list.
But if its invalid i need to reset the value to original.
I have tried calling StateHasChange() but that didn't helps.
Can anybody helps?
Try this:
private async Task DataChange(ChangeEventArgs e, PnL pnl)
{
if(e.Value.ToString().Contains(10))
{
pnl.value = e.Value.ToString();
}
else
{
var tempValue = pnl.value;
pnl.value = string.Empty();
await Task.Yield();
pnl.value = tempValue;
}
}
So what happens here is this.
The code runs synchronously to the await Task.Yield.
At this point pnl.value is a blank string.
DataChange yields execution to the Blazor event handler which loads a render action onto the Renderer Queue.
The Yield allows the Renderer to service it's render queue, and re-render the component. pnl.value is blank - different from the last render - so the input gets updated in the DOM.
The code after the yield now runs to completion, setting pnl.value back to it's original value.
DataChange completes and the Blazor event handler loads a second render action onto the Renderer Queue which now re-renders the input as the value has changed again.
In your code everything runs synchronously. You can call StateHasChanged as many times as you like, the Renderer only gets task time to run it's queue after DataChange completes. At this point the Renderer still thinks the input is set to the original value of pnl.value (not the edited value), so the DOM doesn't get updated.
There's an article on CodeProject with more detail - https://www.codeproject.com/Articles/5310624/Blazor-UI-Events-and-Rendering
When the #onchange is happening, the binding is already over, so the invalid string is at that time in pnl.value.
One of the solution could be an extra property for binding. The value property would only change when the tempVal is valid, otherwise it would reset the tempVal:
<input type="text" #bind-value="item.tempVal" />
#code {
PnL item = new() { value = "hello10hi", tempVal = "hello10hi" };//valid values
class PnL
{
public string value { get; set; }
string _tempVal;
public string tempVal { get => _tempVal; set { _tempVal = value; DataChange(value, this); } }
void DataChange(string newVal, PnL pnl)
{
if (newVal.Contains("10"))//is valid
{
pnl.value = newVal;
}
else
{
pnl.tempVal = pnl.value;
}
}
}
}
EDIT
(The code has been updated).
The problem was, that the tempVal was not updating when writing, so it still had the default value, so no change after assigning the value, thus no update in UI.
With bind-value the tempVal will be always updated. The "problem" there is that we loose the onechange code (because we use bind). Thus I had to interact with changes inside the tempVal property.
Related
I am not quite sure if I am asking the right question. I assume other people have had this issue.
I built my own Blazor Grid component. I am using an bound to a property.
I have a function to load my grid. I changed my bound property to a full getter,setter. In the setter, I call my function to load the grid. This works fast and easy in pretty much all instances. But, I have one grid that when binding it will take a few extra seconds to complete.
The problem: I can't seem to figure out how to get my waiting spinner component to show when loading my grid.
Example Blazor Markup:
#if (dataGrid == null)
{
<hr />
<BitcoSpinner></BitcoSpinner>
}
else
{
<BitcoGrid TheGrid="dataGrid"></BitcoGrid>
}
Here is my property and GridLoading:
private string selectedGroup1 = "";
public string selectedGroup
{
get => selectedGroup1;
set
{
selectedGroup1 = value;
LoadGrid();
}
}
private void LoadGrid()
{
dataGrid = null;
PT_Grid_Admin ptGrid = new PT_Grid_Admin(permitTraxLibrary, gridParams);
dataGrid = ptGrid.ADMIN_FeeList(feeList.Fee_Key, selectedGroup);
}
You should define LoadGrid method asynchronously. Therefore, at the beginning of the program, when the data grid value is set, your spinner will be displayed until the data grid value is not received. Then, after receiving the data grid value, the else part of the condition will be executed and its value will be displayed to the user.
It may not take much time to receive information from the DB in local mode, so the following code can be used to simulate the delay:
System.Threading.Thread.Sleep(5000);
In general, I think that if your code changes like this, you can see the spinner.
private string selectedGroup1 = "";
public string selectedGroup
{
get => selectedGroup1;
set
{
selectedGroup1 = value;
LoadGrid();
}
}
private async Task LoadGrid()
{
dataGrid = null;
System.Threading.Thread.Sleep(5000);
.
.
}
Of course, it is better to load the datagrid in OnInitializedAsync method. For more info you can refer to this link.
I have an interesting problem. I'm creating a Markdown Editor component for Blazor. The component is working fine. Now, I discover an interesting issue related to a JSInvokable. So, the component has a Value property
[Parameter]
public string Value { get; set; }
When I add the component to a page like this
<MarkdownEditor Value="#markdownValue"
ValueChanged="#OnMarkdownValueChanged"
ValueHTMLChanged="#OnMarkdownValueHTMLChanged" />
#code {
string markdownValue = "Test";
}
So, in the JavaScript, every time the content changes, I call the JSInvokable via DotNetObjectReference.
The function is looking like that (here the full code)
[JSInvokable]
public Task UpdateInternalValue(string value)
{
Value = value;
return ValueChanged.InvokeAsync(Value);
}
When I debug the function, I can see the new value in the value
Then, I can see the Value parameter is changed with the new value
But a line after that Value has again the initial value and not the new value from the parameter.
I don't know if this is something related to the JSInvokable function. The interesting thing is in the some function I have another variable ValueHTML that changes its value as expected.
This is a variation on "Component changing its Parameter value" , which you shouldn't do.
ValueHTML works because you don't specify it in the owning component. But the await there enables the StateHasChanged related logic to refresh Value from he surrounding component.
I think you need to do
// Value = value;
and let
await ValueChanged.InvokeAsync(Value);
make the change, through the owning component.
So, based on the comment from Henk, I googled "Component changing its Parameter value" and I found the solution.
In the component, I have to change the Value parameter like that
[Parameter]
public string Value {
get => _value;
set
{
if (_value == value) return;
_value = value;
ValueChanged.InvokeAsync(value);
}
}
private string _value;
Then, in the page, I call the component with a bind-Value
<MarkdownEditor #bind-Value="#markdownValue"
ValueHTMLChanged="#OnMarkdownValueHTMLChanged" />
In UpdateInternalValue:
Value = value;
//....
ValueHTMLChanged.InvokeAsync(ValueHTML);
return ValueChanged.InvokeAsync(Value);
You set Value. You shouldn't do this - This is bad practice.
You async invoke the ValueHTMLChanged event callback.
This calls OnMarkdownValueHTMLChanged in the parent.
This triggers a StateHasChanged event in the parent.
The Renderer calls SetParamatersAsync on the component
Value is set back to the old value of markdownValue. It hasn't yet been updated!
You're now back in the code block and async invoke the ValueChanged event callback.
repeat steps 3....5 In 6 markdownValue is set to the new value so shows correctly.
You're living in the async world: things don't get run in a nice sequential order.
Don't set Value. I think, if I've read the code right, it will get updated as part of calling ValueChanged and the triggered component rendering process.
At first I will post the code, it is short and quite clear.
cb_currentProfile is a ComboBox filled with 3 items when form is loaded:
delegate void SetCurrentProfileCallback(int index);
private void SetCurrentProfile(int index) // Set index of Combobox.SelectedItem
{
if (this.cb_currentProfile.InvokeRequired)
{
SetCurrentProfileCallback d = new SetCurrentProfileCallback(SetCurrentProfile);
this.Invoke(d, new object[] { index });
}
else
{
this.cb_currentProfile.SelectedItem = 2; // Won't work
this.cb_currentProfile.Visible = false; // It works
}
}
The problem is that when I try to change SelectedItem property, then it won't do nothing (no crash, just nothing happens).
I am sure that this code is reached in my form application.
At now I am making it in .NET 4.6 (but it was not working in v4.5 either)
The place where I am calling this method is in Task body:
Task.Run(() =>
{
while(true)
{
// ...
SetCurrentProfile(2);
// ...
Thread.Sleep(100);
}
});
I think that the problem is related to DataSource that seems to be invisible by other thread than main UI's.
I am also sure that data are loaded to ComboBox before code reaches a Task creation.
Edit 1 - selected item is null, Count property returns 0
When I used a debugger to check for some data, the results are:
var x = this.cb_currentProfile.SelectedItem; // null
var y = this.cb_currentProfile.Items.Count; // 0
It looks like, with the this.cb_currentProfile.SelectedItem = 2 statement, you intend to set the selection of the ComboBox by index. The ComboBox.SelectedItem Property accepts an Object and attempts to find it in its collection of items, selecting it if successful, and doing nothing otherwise. To select a particular index of the ComboBox, set the ComboBox.SelectedIndex Property instead.
When the DataGridView in my application is populated, the following method is fired:
public void OrderSelectionChanged()
{
ConfirmOrCancelChangesDialog();
// Get values from selected order and populate controls
if (view.OrderTable.SelectedRows.Count != 0)
{
OrderViewObject ovm = (OrderViewObject)view.OrderTable.SelectedRows[0].DataBoundItem;
selectedOrder = orderModel.GetOrderById(ovm.OrderId);
// Populate view controls with data from selected order
view.OrderID = selectedOrder.Id.ToString();
---->> view.OrderDateCreated = selectedOrder.DateCreated; <<-----
view.OrderDeliveryDate = selectedOrder.DeliveryDate;
PopulateOrderAddressControls(selectedOrder.Address);
PopulateOrderItemTableControl();
PopulateOrderWeightAndSumControls();
view.OrderNote = selectedOrder.Note;
// Enable buttons
view.DeleteOrderButtonEnabled = true;
view.NewOrderItemButtonEnabled = true;
}
else
{
view.DeleteOrderButtonEnabled = false;
}
}
For some reason, the "isSaved" variable is being changed from true to false at the row I marked with arrows and I can't figure out why. This is not supposed to happen and was never an issue before, but suddenly appeared.
The variable "isSaved" is being checked in the following method:
public void ConfirmOrCancelChangesDialog()
{
if (!isSaved)
{
DialogResult dialog = MessageBox.Show(Properties.Resources.SaveChanges,
Properties.Resources.SaveChangesTitle, MessageBoxButtons.YesNo);
if (dialog == DialogResult.Yes)
{
SaveOrder();
}
else
{
UndoChanges();
}
}
}
This is causing the save or cancel dialog to appear everytime the application is started, which obviously is wrong. Since the selection changed method is fired three times and isSaved is changed during the first run, the dialog pops up during the second time around. Through debugging step by step I could figure out at what point isSaved is changing, but not how or why.
View is the form, OrderDateCreated is a getter/setter for a DateTimePicker, selectedOrder is just an order object and DateCreated a Date. Am I missing something here?
Cheers!
It seem related to your code setting values to view object.
If you didn't create properties by your own, that object may have set properties which detects any change and setting the isSaved properties to false.
Try this workaround:
bool wasSaved = isSaved; //reference properly to your isSaved variable, and store it in the wasSaved local var
view.OrderID = selectedOrder.Id.ToString();
view.OrderDateCreated = selectedOrder.DateCreated;
view.OrderDeliveryDate = selectedOrder.DeliveryDate;
isSaved = wasSaved; //revert to the previous state
Trying to work out this whole web part personalisation, and trying to implement it for a list box.
Well the end result will be two list boxes, with interchangeable values (ie, a value will only exist in one of the listboxes)
But I can't maintain the datasource for it. So maybe I'm going about it wrong?
This is what I have for a test H2 tag on the page
[Personalizable(PersonalizationScope.User)]
public string LabelText {
get { return h2Test.InnerText; }
set { h2Test.InnerText = value; }
}
And it works fine, if I have a textbox and use it to change the value of LabelText, then when I close the browser it automagically persists the change.
So I thought, ok, then maybe the same will work with a list box
[Personalizable(PersonalizationScope.User)]
public DomainList Domains {
get { return (DomainList)lstBxDomains.DataSource; }
set {
lstBxDomains.DataSource = value;
lstBxDomains.DataBind();
}
}
Where DomainList is just a class which extends List, and Domain is just a three field class, int, string, string.
But it doesn't, so is this too complicated for the webpart personalisation automagican, or have i just implement it wrongly (Which is more than likely)
This is my event handler to remove the items from the list:
protected void btnRemDomain_Click(object sender, EventArgs e) {
if (IsPostBack && lstBxDomains.SelectedIndex > -1) {
for (int i = 0; i < lstBxDomains.Items.Count; i++) {
if (lstBxDomains.Items[i].Selected) {
Domains.Remove(Domains.Find(d => d.ID.ToString() == lstBxDomains.Items[i].Value));
}
}
Domains = Domains;
}
}
The Domains=Domains; line is in there to see if explicitly setting the value made a difference (as Removing doesn't acutally reset the value of the field), but it doesn't. I've also tried creating a new local DomainList setting it to the global one, and then doing the remove/find on it, and then setting the local one to the global. But not working either.
I have managed to resolve this by using WebPart.SetPersonalizationDirty(this); in the set accessor of Domains, but would someone mind confirming if this is an appropriate way to do it?