I dont know if it possible but what I want is something like that
In WinForm listbox1 has a list of lines(copied from file)
In another Thread and class I run on a List that contains the same lines each line I parse and DoSomething
once I finish with that line I want the index in the listbox to change
from my basic and limited understanding my approach was with an Event to fire in form and than maybe using Invoke (for not to cross thread )
Is there is a way to somehow bind to index of the listbox somehow with my for/foreach loop ?
class form
{
listBoxScript.SetSelected(ScriptCounter, true);<--bind the ScriptCounter?
}
in another Class
class RunScript
{
//..
public void RunScriptList()
{
ScriptCounter = 0 ;
foreach ( var cell in ScriptList)
{
ScriptCounter ++;
//DoSomething
}
}
}
Make sure you implement INotifyPropertyChanged in RunScript class. Here's a complete sample:
class RunScript : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int scriptCounter;
private ISynchronizeInvoke invoker;
public RunScript(ISynchronizeInvoke invoker)
{
if (invoker == null) throw new ArgumentNullException("invoker");
this.invoker = invoker;
}
public async void RunScriptList()
{
ScriptCounter = 0;
foreach (var cell in Enumerable.Range(1, 15))
{
ScriptCounter++;
await Task.Delay(1000);
//DoSomething
}
}
public int ScriptCounter
{
get { return scriptCounter; }
set
{
scriptCounter = value;
OnPropertyChanged();
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
{
Action action = () => handler(this, new PropertyChangedEventArgs(propertyName));
invoker.Invoke(action, null);
}
}
}
private RunScript rs;
public Form1()
{
InitializeComponent();
rs = new RunScript(this)
var binding = new Binding("SelectedIndex", rs, "ScriptCounter", false, DataSourceUpdateMode.OnPropertyChanged);
listBox1.DataBindings.Add(binding);
}
private void button1_Click(object sender, EventArgs e)
{
rs.RunScriptList();
}
Note I have used async/await in RunScriptList, If you do it in another thread you need to fire PropertyChanged event in main thread to avoid cross thread exception.
Related
I am working on a project in which I need to notify my GUI that my list has new items.
I tried this with an observablecollection, but I use timers, and when I try to add or remove items from the observablecollection, then an exception is thrown that the collection is being marshalled from another thread.
Therefore, I started thinking about using the PropertyChanged event. However, I have trouble with getting it to work. What I understand is that it is used for properties. I have tried the following code without success:
public class MyCollection : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ConcurrentQueue<PC_Info_Item> data;
public List<string> table;
public MyCollection()
{
data = new ConcurrentQueue<PC_Info_Item>();
table = new List<string>();
}
public void Add(PC_Info_Item item)
{
data.Enqueue(item);
OnPropertyChanged(nameof(table));
}
public void Add(string item)
{
table.Add(item);
OnPropertyChanged(nameof(table));
}
public void delete()
{
data.TryDequeue(out PC_Info_Item item);
OnPropertyChanged(nameof(table));
}
public void delete_string(string item)
{
table.Remove(item);
OnPropertyChanged(nameof(table));
}
protected void OnPropertyChanged(string name = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
Is there a way to make this work for lists?
you can wrap the calls into lock, that will make other threads wait till the call is finished.
private readonly object CollectionLock = new object();
public void Add(PC_Info_Item item)
{
lock(CollectionLock) {
data.Enqueue(item);
}
}
public void Add(string item)
{
lock(CollectionLock) {
table.Add(item);
}
}
public void delete()
{
lock(CollectionLock) {
data.TryDequeue(out PC_Info_Item item);
}
}
public void delete_string(string item)
{
lock(CollectionLock) {
table.Remove(item);
}
}
I think you should call on property changed as below where we should get the invocation list and call invoke on each receiver with lock.
private object _lock = new object();
protected void OnPropertyChanged(string name = "")
{
var receivers = this.PropertyChanged.GetInvocationList();
foreach (EventHandler<PropertyChangedEventArgs> receiver in receivers)
{
lock (this._lock)
{
receiver?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
}
I have a view model that has several properties that are databound to several controls.
When I raise PropertyChanged on one of them, the controls unexpectedly all update. I would expect only the one I am raising the event on to update.
For my form, I have this:
public partial class MainForm : Form
{
AmountCalculatorVM amountCalculatorVM;
public MainForm()
{
InitializeComponent();
}
private void setBindings()
{
textBoxTotalAmount.DataBindings.Add("Text", amountCalculatorVM, "TotalAmount");
textBoxAverage.DataBindings.Add("Text", amountCalculatorVM, "Average",true, DataSourceUpdateMode.Never,null, "#.00");
textBoxCount.DataBindings.Add("Text", amountCalculatorVM, "Count");
listBoxLineAmounts.DataSource = amountCalculatorVM.Amounts;
}
private void MainForm_Load(object sender, EventArgs e)
{
amountCalculatorVM = new AmountCalculatorVM();
setBindings();
}
private void buttonAddAmount_Click(object sender, EventArgs e)
{
if (int.TryParse(textBoxLineAmount.Text.Replace(",", ""), out int amount))
{
amountCalculatorVM.Amounts.Add(amount);
textBoxLineAmount.Text = "";
textBoxLineAmount.Focus();
}
}
private void buttonClear_Click(object sender, EventArgs e)
{
textBoxLineAmount.Text = "";
amountCalculatorVM.Amounts.Clear();
textBoxLineAmount.Focus();
}
}
Then, for my view model, I have this:
class AmountCalculatorVM : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private readonly AmountList amounts;
public BindingSource Amounts { get; }
public int TotalAmount => amounts.TotalAmount;
public int Count => amounts.Count;
public decimal Average => amounts.Average;
public AmountCalculatorVM()
{
amounts = new AmountList();
Amounts = new BindingSource();
Amounts.DataSource = amounts;
Amounts.ListChanged += Amounts_ListChanged;
}
private void Amounts_ListChanged(object sender, ListChangedEventArgs e)
{
//Any one of these will cause all three textboxes to update in the form
//I would think that with Count and Average commented out, the Count and
//Average textboxes would not update.
OnPropertyChanged("TotalAmount");
//OnPropertyChanged("Count");
//OnPropertyChanged("Average");
//Using any other word will not
//OnPropertyChanged("SomeOtherRandomWord");
}
}
Here is the AmountList class for reference:
class AmountList : List<int>
{
public int TotalAmount
{
get
{
int total = 0;
foreach (int amount in this)
{
total += amount;
}
return total;
}
}
Now, unexpectedly, all three textboxes update if an item is added to the amounts list, which fires ListChanged, and then in turn, the PropertyChanged event.
It doesn't matter which of the three properties I fire PropertyChanged on, but it won't work if I use a different value - it needs to be either TotalAmount, Count, or Average.
I can't understand this behaviour. I would have expected only the text box bound to TotalAmount to be updated, and not the other two, since nothing seems to be notifying them that an update has occurred.
Any ideas?
Why don't you implement the propertychanged like this:
public class Data : INotifyPropertyChanged
{
// boiler-plate
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetField<T>(ref T field, T value, string propertyName)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
// props
private string name;
public string Name
{
get { return name; }
set { SetField(ref name, value, "Name"); }
}
}
You can control now, in the setter, which property fires the event:
private string name;
public string Name
{
get { return name; }
set { SetField(ref name, value, "Name"); }
}
you know what I mean?
I am updating a Datagrid and when a user inputs a number that already exists I want notify the user they the number already exists and then clear the value from the datagrid.
I know why this is happening, but I can't figure out how to stop this or how to make a work around.
This is very simplified code: Using EF code first with MVVM model.
public partial class StaffMasterData
{
public System.Guid Id { get; set; } // ID (Primary key)
public int? StaffNo { get; set; } // StaffNo
public StaffMasterData()
{
InitializePartial();
}
partial void InitializePartial();
}
Entity extension class for StaffMasterData :
public partial class StaffMasterData : INotifyPropertyChanged
{
partial void InitializePartial()
{
Id = Guid.NewGuid();
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
handler?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
And the method to save the data:
public void SaveMasterData(StaffMasterData nwRowData)
{
using (var db = CreateDbContext())
{
//MasterDataBinding is the observableCollection
//the datagrid is being bound to.
var staffNoExists = MasterDataBinding.Any(p => p.StaffNo == nwRowData.StaffNo);
if (!staffNoExists)
{
db.StaffMasterDatas.AddOrUpdate(nwRowData);
db.SaveChanges();
}
else
{
Alerts.Error("Staff Number exists");
nwRowData.StaffNo = null;
}
}
}
And the assinging of the collection changed event:
public class ShiftManagerViewModel : INotifyPropertyChanged
{
private ObservableCollection<StaffMasterData> _mMasterDataBinding = new ObservableCollection<StaffMasterData>();
public ObservableCollection<StaffMasterData> MasterDataBinding
{
get { return _mMasterDataBinding; }
set
{
if (value != _mMasterDataBinding)
{
_mMasterDataBinding = value;
OnPropertyChanged();
}
}
}
public ShiftManagerViewModel()
{
_mMasterDataBinding.CollectionChanged += collectionChanged_Event;
}
private void collectionChanged_Event(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null && e.NewItems.Count > 0)
{
foreach (INotifyPropertyChanged item in e.NewItems.OfType<INotifyPropertyChanged>())
{
item.PropertyChanged += propertyChanged_Event;
}
}
if (e.OldItems != null && e.OldItems.Count > 0)
{
foreach (INotifyPropertyChanged item in e.OldItems.OfType<INotifyPropertyChanged>())
{
item.PropertyChanged -= propertyChanged_Event;
}
}
}
public void propertyChanged_Event(object sender, PropertyChangedEventArgs e)
{
if (sender is StaffMasterData)
{
SaveMasterData((StaffMasterData)sender);
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
handler?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
As it is probably very clear, when running through this line of code nwRowData.StaffNo = null; , it fires the event again as the collection has been modified which then in turn runs through the messageBox code and it pops up twice.
Honestly I have hit a brick wall with this and any point in the right direction would be appreciated.
You could use a flag that determines whether to actually call the SaveMasterData method. Set this flag to false just before you set the StaffNo property to null and then set it back to true immediately afterwards:
private bool _handle = true;
public void SaveMasterData(StaffMasterData nwRowData)
{
using (var db = CreateDbContext())
{
//MasterDataBinding is the observableCollection
//the datagrid is being bound to.
var staffNoExists = MasterDataBinding.Any(p => p.StaffNo == nwRowData.StaffNo);
if (!staffNoExists)
{
db.StaffMasterDatas.AddOrUpdate(nwRowData);
db.SaveChanges();
}
else
{
Alerts.Error("Staff Number exists");
_handle = false;
nwRowData.StaffNo = null;
_handle = true;
}
}
}
public void propertyChanged_Event(object sender, PropertyChangedEventArgs e)
{
if (!_handle && sender is StaffMasterData)
{
SaveMasterData((StaffMasterData)sender);
}
}
Trying to get clear about flaw in this code:
Scenario 1:
This scenario uses data binding and causes the very well known cross-thread exception in the NotifyPropertyChanged() method in the PriceSimulator class.
Scenario 2:
This scenario solves the problem by subscribing to the PropertyChanged event of PriceSimulator, eliminates the cross-thread issue but has to avoid data binding altogether.
Assuming Scenario 1 was the intended scenario and assuming one has no knowledge of the inner workings of PriceSimulator and just wanted to bind to the Price property, what is the core issue here?
Form1.cs:
public partial class Form1 : Form
{
PriceSimulator simul;
Action labelAction;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
labelAction = new Action(SetLabel);
simul = new PriceSimulator(5, 1000);
//Scenario 1:
//Use data binding and get Cross-Thread exception
//label1.DataBindings.Add("Text", simul, "Price");
//Scenario 2:
//This works fine
//Subscribe to PropertyChanged event
simul.PropertyChanged += task_PropertyChanged;
simul.Start();
}
//Scenario 2:
void task_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (label1.InvokeRequired)
Invoke(labelAction);
else SetLabel();
}
private void SetLabel()
{
label1.Text = simul.Price.ToString("C2");
}
}
PriceSimulator.cs:
public class PriceSimulator : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int max, delay, priceValue;
private Timer timer;
public PriceSimulator(int max, int delay)
{
this.max = max;
this.delay = delay;
}
public void Start()
{
timer = new Timer(CallbackProc, null, delay, delay);
}
private void CallbackProc(object obj)
{
if (++Price >= max)
timer.Dispose();
}
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
try
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
catch (Exception ex)
{
timer.Dispose();
System.Windows.Forms.MessageBox.Show(ex.Message);
}
}
public int Price
{
get
{
return priceValue;
}
set
{
if (priceValue != value)
{
priceValue = value;
NotifyPropertyChanged();
}
}
}
}
You have to have the current context in your PriceSimulator class:
private readonly SynchronizationContext _context = SynchronizationContext.Current;
Now that you have the context, you can use it to update the UI:
_context.Post(delegate
{
if (++Price >= max)
timer.Dispose();
}, null);
I am hoping to get some pointers on what I am missing in my code.
I have a text box bound to a object property that is an item in the list, and that value doesnt update on the form if I request another item in the list.
To illustrate with example below:
txtGain value is populated after openJSONRequestFileToolStripMenuItem_Click fuction
Once I select something different in cmbSignals combobox, I expect the txtGain value to become updated since SelectedChannel is updated as well, which in turn updates the selectedindex but it doesn't happen.
Basically I want to have my txtGain value updated based on what I select in the cmbSignals. Obviously the binding is there so that I can modify the value in the text box and have it be updated in the property its bound to.
I suspect that I have to somehow force update the bindings but not sure how to do that. Any help would be appreciated.
public partial class MainForm : Form
{
private MyData req;
public MainForm()
{
InitializeComponent();
cmbSignals.DisplayMember = "Name";
cmbSignals.ValueMember = "Value";
}
private void openJSONRequestFileToolStripMenuItem_Click(object sender, EventArgs e)
{
string text = File.ReadAllText("sample.json");
req = new MyData(JsonConvert.DeserializeObject<SerializedRequest>(text));
cmbSignals.DataSource = req.SignalNames;
cmbSignals.SelectedValue = req.SelectedChannel;
SetBindings();
}
private void SetBindings()
{
txtGain.DataBindings.Add(new Binding("Text", req, "Gain"));
}
private void cmbSignals_SelectedValueChanged(object sender, EventArgs e)
{
req.SelectedChannel = Convert.ToInt32(cmbSignals.SelectedValue);
}
}
public class MyData : INotifyPropertyChanged
{
private SerializedRequest Data = new SerializedRequest();
private int selectedIndex = 0;
public int SelectedChannel
{
get
{
return selectedIndex + 1;
}
set
{
this.selectedIndex = value - 1;
}
}
public string Gain
{
get
{
return Data.signals[selectedIndex].gain;
}
set
{
Data.signals[selectedIndex].gain = value;
OnPropertyChanged("Gain");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
public List<SignalsCmbItem>SignalNames
{
get
{
List<SignalsCmbItem>channels = new List<SignalsCmbItem>();
for(int i = 0; i<Data.signals.Count;i++)
{
channels.Add(new SignalsCmbItem { Value = i + 1, Name = i+1 + " - " + Data.signals[i].label });
}
return channels;
}
}
}
Pretty annoying "feature", isn't it?.
But no worries, to get around this, add one line of code inside your cmbSignals_SelectedValueChanged(sender, e) method, after you change value of req.SelectedChannel.
txtGain.BindingContext = new BindingContext();