WPF DataGrid Multithreading Crash - c#

I have a View with a DataGrid in it.
A ViewModel as DataContext where i can access a DataTable in a background object.
The background object has to work with the DataTable and keep it updated.
The user has also be allowed to make changes to that DataTable.
If i create a copy of the DataTable it stops crashing but the user is obviousely not working on the data.
If i leave access open for the user the program crashed inevitabely.
Here is a short program that will crash:
app.cs
public partial class App : Application
{
public App()
{
SomeBackgroundThing background = new SomeBackgroundThing();
MainWindowViewModel viewmodel = new MainWindowViewModel(background);
MainWindowView view = new MainWindowView(viewmodel);
view.Show();
}
}
main xaml
<Window x:Class="NullPointerDataGrid.MainWindowView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DataGrid Name="datagrid" ItemsSource="{Binding Path=table, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
</Grid>
</Window>
and the program code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.Data;
using System.Diagnostics;
using System.Timers;
using System.ComponentModel;
namespace NullPointerDataGrid
{
public partial class MainWindowView : Window
{
public MainWindowView(MainWindowViewModel model)
{
DataContext = model;
InitializeComponent();
datagrid.Loaded += new RoutedEventHandler(ScrollToBottom);
datagrid.AutoGeneratedColumns += new EventHandler(StarSizeLastRow);
}
void ScrollToBottom(object sender, RoutedEventArgs e)
{
Debug.WriteLine("TableGrid_ScrollToBottom");
if (datagrid.Items.Count > 0)
{
var border = VisualTreeHelper.GetChild(datagrid, 0) as Decorator;
if (border != null)
{
var scroll = border.Child as ScrollViewer;
if (scroll != null) scroll.ScrollToEnd();
}
}
}
void StarSizeLastRow(object sender, EventArgs e)
{
Debug.WriteLine("TableGrid_StarSizeLastColumn");
try
{
datagrid.Columns[datagrid.Columns.Count - 1].Width = new DataGridLength(1, DataGridLengthUnitType.Star);
}
catch { }
}
}
public class MainWindowViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private SomeBackgroundThing thing;
public DataTable table
{
get
{
lock (thing.table)
{
//DataTable wpfcopy = thing.table.Copy();
return thing.table;
};
}
set
{
Debug.Write("This never happens");
}
}
public MainWindowViewModel(SomeBackgroundThing thing)
{
this.thing = thing;
thing.Changed += new EventHandler(thing_Changed);
}
void thing_Changed(object sender, EventArgs e)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("table"));
}
}
}
public class SomeBackgroundThing : IDisposable
{
public DataTable table;
private DataTable tablecopy;
private System.Timers.Timer timer, slowrowchanger;
public event EventHandler Changed = new EventHandler((o, e) => { ;});
protected void CallChanged(object sender, EventArgs e)
{
Changed(sender, e);
}
public SomeBackgroundThing()
{
CreateTable();
UpdateB(this, null);
tablecopy = table.Copy();
InitAndStartTimer(1);
}
#region timer
private void UpdateA()
{
Boolean haschanged = false;
DataTable newcopy = table.Copy(); ;
if (newcopy.Rows.Count != tablecopy.Rows.Count)
{
Debug.WriteLine("Different ammount of rows");
haschanged = true;
}
else if (newcopy.Columns.Count != tablecopy.Columns.Count)
{
Debug.WriteLine("Different ammount of columns");
haschanged = true;
}
else
{
for (int i = 0; i < newcopy.Rows.Count; i++)
{
for (int j = 0; j < newcopy.Columns.Count; j++)
{
if (newcopy.Rows[i][j].ToString() != tablecopy.Rows[i][j].ToString())
{
Debug.WriteLine(String.Format(
"Element [{0}/{1}]: {2} is different from {3}",
i, j, newcopy.Rows[i][j], tablecopy.Rows[i][j]
));
haschanged = true;
}
if (haschanged) break;
}
if (haschanged) break;
}
}
if (haschanged)
{
tablecopy = newcopy;
}
}
private void InitAndStartTimer(int interval)
{
timer = new System.Timers.Timer();
timer.Interval = interval;
timer.AutoReset = true;
timer.Elapsed += new ElapsedEventHandler((s, e) =>
{
UpdateA();
});
timer.Enabled = true;
slowrowchanger = new System.Timers.Timer();
slowrowchanger.Interval = 3000;
slowrowchanger.AutoReset = true;
slowrowchanger.Elapsed += new ElapsedEventHandler((s, e) =>
{
UpdateB(null, null);
});
slowrowchanger.Enabled = true;
}
public void Dispose()
{
timer.Enabled = false;
slowrowchanger.Enabled = false;
timer.Dispose();
slowrowchanger.Dispose();
}
#endregion
#region editlastrow
void UpdateB(object sender, EventArgs e)
{
Random rnd = new Random();
List<String> cells = new List<string>{
"The SAME",
rnd.Next(0,100).ToString(),
rnd.ToString(),
rnd.NextDouble().ToString()};
lock (table)
{
OverwriteOrAppendLastRow(ref table, cells);
table.AcceptChanges();
}
CallChanged(this, null);
}
private void OverwriteOrAppendLastRow(ref DataTable table, List<string> newrow)
{
if (table.Rows.Count == 0) CreteEmptyRow(ref table);
if (newrow[0].ToString() != table.Rows[table.Rows.Count - 1][0].ToString())
{
Debug.WriteLine(String.Format("Creating row because '{0}' is different from '{1}'", newrow[0], table.Rows[table.Rows.Count - 1][0]));
CreteEmptyRow(ref table);
}
OverwriteLastRow(ref table, newrow);
}
private void OverwriteLastRow(ref DataTable table, List<string> newrow)
{
for (int i = 0; i < newrow.Count() && i < table.Columns.Count; i++)
{
table.Rows[table.Rows.Count - 1][i] = newrow[i];
}
}
private void CreteEmptyRow(ref DataTable table)
{
table.Rows.Add(new String[table.Columns.Count]);
}
#endregion
private void CreateTable()
{
table = new DataTable();
table.Columns.Add("FirstCell", typeof(String));
table.Columns.Add("BananaCell", typeof(String));
table.Columns.Add("CherryCell", typeof(String));
table.Columns.Add("Blue", typeof(String));
Random rnd = new Random();
for (int i = 0; i < 145; i++)
{
table.Rows.Add(new String[]{
rnd.Next().ToString(),
rnd.Next(0,i+1).ToString(),
rnd.ToString(),
rnd.NextDouble().ToString()});
}
}
}
}
How can i stop this multithread crashing?
EDIT:
I don't know if there are more than one reasons for this code to crash. But i did my best to gather some information about one reason to crash:
Nullpointer exception in App.g.cs - the autogenerated portion. The Debugger wont step into it - so i cant say anything about the line it crashes in.
Here is the Exception Detail, sorry for the German.
System.NullReferenceException wurde nicht behandelt.
Message=Der Objektverweis wurde nicht auf eine Objektinstanz festgelegt.
Source=PresentationFramework
InnerException:
The Stacktrace only shows "Externer Code" so no stack to trace.
The thing is that WPF crashes - my code can handle it... somehow i need to capsule WPF so it wont crash, one way to do that is to copy the DataTable - but then i loose the ability to write back that table since its setter is not called when something has gotten edited.
EDIT #2:
I recreated this example to show the error i have in another program and i just found out that what crashes is actually related with the scrollbar. If i change the ammount of displayed data to a low number so that there is no scrollbar, the code will not crash.

The following change to the viewmodel solves the problem.
I am now using a copy for wpf to work on and only note the canges should they occur. This code has an issue with the poorly refined change mechanism - but that is beyond the scope of this question.
public class MainWindowViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private SomeBackgroundThing thing;
private DataTable wpftable;
public DataTable table
{
get
{
lock (wpftable)
{
return wpftable;
}
}
set
{
lock (wpftable)
{
wpftable = value;
}
}
}
public MainWindowViewModel(SomeBackgroundThing thing)
{
wpftable = thing.table.Copy();
this.thing = thing;
thing.Changed += new EventHandler(thing_Changed);
}
void thing_Changed(object sender, EventArgs e)
{
if (PropertyChanged != null)
{
DataTable wpftablecopy = wpftable.Copy();
DataTable thintablecopy = thing.table.Copy();
int rowcount = wpftablecopy.Rows.Count;
for (int col = 0; col < 4; col++)
{
for (int row = 0; row < rowcount; row++)
{
if (wpftablecopy.Rows[row][col] != thintablecopy.Rows[row][col])
wpftable.Rows[row][col] = thintablecopy.Rows[row][col];
}
}
PropertyChanged(this, new PropertyChangedEventArgs("table"));
}
}
}

Related

Crossthread databinding in NotifyPropertyChanged for DataGridView Control

When trying to update a displayTimer (duration for how long a specific orderline is taking), I get a Crossthread error in NotifyPropertyChanged for my DisplayTimer Property. The Control it mentions is a datagridview inside a tableLayoutPanel that I generate in code in a form.
protected virtual void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged != null)
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public string DisplayTimer
{
get
{
return _displayTimer;
}
set
{
_displayTimer = value;
NotifyPropertyChanged("DisplayTimer");
}
}
I set this in an static class with a System.timers.timer elapsed event.
Code looks like this:
public static BindingList<OrderModel> Orders = new();
private static void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
try
{
timer.Stop();
BindingList<OrderModel> newOrders = DBQueries.GetOrders();
foreach (OrderModel order in newOrders)
{
Orders.Add(order);
}
DBQueries.OrderUpdates();
foreach (OrderModel om in KitchenComHandler.Orders)
{
for (int i = 0; i < om.Orderlines.Count; i++)
{
OrderlineModel ol = om.Orderlines[i];
TimeSpan ts = DateTime.Now - ol.ReceivedTime;
ol.DisplayTimer = ts.ToString(#"hh\:mm\:ss"); <-- the Culprit
}
}
}
catch(Exception ex)
{
Debug.WriteLine(ex);
}
finally
{
timer.Start();
}
}
Additional information: I've tried switching the DataGridView.DataBinding to BindingSource as I read that this should fix certain crossthreading issues.
The OrderModel contains:
public BindingList<OrderlineModel> Orderlines
{
get
{
if (_orderlines == null)
{
_orderlines = new();
_orderlines.ListChanged += Orderlines_ListChanged;
}
return _orderlines;
}
}
private void Orderlines_ListChanged(object sender, ListChangedEventArgs e)
{
NotifyPropertyChanged("Orderlines");
}
Note: I managed to get this error for a no-name control as well.
I can update all the other properties in another class as well, however it doesn't make any sense to have the updated displaytimer here as the following method only occures when there is an update to get ->
if (fullUpdate)
{
orderline.OrderID = reader.GetInt32(0);
orderline.ID = reader.GetInt32(1);
orderline.Description = reader.GetString(3);
orderline.Status = OrderAndOrderlineStatus.Received; //We don't want to touch the status for an existing orderline.
orderline.StatusChanged = true;
}
orderline.CountOf = reader.GetDecimal(2);
Byte[] b = (Byte[])reader.GetValue(5);
orderline.RowVersion = b;
if (!string.IsNullOrWhiteSpace(reader.GetString(4)))
{
orderline.Alternatives = new();
string[] separatingString = { Environment.NewLine };
string[] altArray = reader.GetString(4).Split(separatingString, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < altArray.Length; i++)
{
orderline.Alternatives.Add(altArray[i]);
}
}
I solved this now by Finding any active form and executing the update of the DisplayTimer property there. Code:
if (System.Windows.Forms.Form.ActiveForm != null &&
System.Windows.Forms.Form.ActiveForm.InvokeRequired)
{
TimeSpan ts = DateTime.Now - ol.ReceivedTime;
System.Windows.Forms.Form.ActiveForm.Invoke(new Action(
() => ol.DisplayTimer = ts.ToString(#"hh\:mm\:ss"))
);
}
EDIT:
I went and updated the code some more and started using SyncronizationContext instead. As I could not instantiate this context from the ApplicationContext I had to find the first form that launched and start my static class containing the update methods.
Code:
UIThread = SynchronizationContext.Current;
TimeSpan totatElapsedTime = (DateTime.Now - Orders[i].Orderlines[j].ReceivedTime);
UIThread.Send((object stat) => {
ol.DisplayTimer = totatElapsedTime.ToString(#"mm\:ss"); //.ToString(#"hh\:mm\:ss");
}, null);

C# Error when setting ListBox.DataSource = null (Possible event firing issue?)

I have searched for a while to find a fix for this and hoped that using the 'SelectionChangeCommitted' event as per this answer ( How to prevent selectedindexchanged event when DataSource is bound? ) would work but unfortunately no such luck.
Basically I want to set my Listbox.DataSource to null, although it contains two objects at the time. It seems like when I hit the line matchupListBox.Datasource = null it jumps to the _SelectedIndexChanged event and enters my other method (LoadMatchup())
If anyone could shed some light on this or advise me on how to set it to null another way (again, SelectionChangeCommitted didn't work) I'd appreciate it. Full code for my class is below:
namespace TrackerUI
{
public partial class TournamentViewerForm : Form
{
private TournamentModel tournament;
List<int> rounds = new List<int>();
List<MatchupModel> selectedMatchups = new List<MatchupModel>();
public TournamentViewerForm(TournamentModel tournamentModel)
{
InitializeComponent();
tournament = tournamentModel;
LoadFormData();
LoadRounds();
}
private void LoadFormData()
{
tournamentName.Text = tournament.TournamentName;
}
private void WireUpMatchupsList()
{
matchupListBox.DataSource = null;
matchupListBox.DataSource = selectedMatchups;
matchupListBox.DisplayMember = "DisplayName";
}
private void WireUpRoundsList()
{
roundDropDown.DataSource = null;
roundDropDown.DataSource = rounds;
}
private void LoadRounds()
{
rounds = new List<int>();
rounds.Add(1);
int currRound = 1;
foreach (List<MatchupModel> matchups in tournament.Rounds)
{
if (matchups.First().MatchupRound > currRound)
{
currRound = matchups.First().MatchupRound;
rounds.Add(currRound);
}
}
WireUpRoundsList();
}
private void roundDropDown_SelectedIndexChanged(object sender, EventArgs e)
{
LoadAllMatchups();
}
private void LoadAllMatchups()
{
int round = (int)roundDropDown.SelectedItem;
foreach (List<MatchupModel> matchups in tournament.Rounds)
{
if (matchups.First().MatchupRound == round)
{
selectedMatchups = matchups;
}
}
WireUpMatchupsList();
}
private void LoadMatchup()
{
MatchupModel m = (MatchupModel)matchupListBox.SelectedItem;
for (int i = 0; i < m.Entries.Count; i++)
{
if (i == 0)
{
if (m.Entries[0].TeamCompeting != null)
{
teamOneName.Text = m.Entries[0].TeamCompeting.TeamName;
teamOneScoreValue.Text = m.Entries[0].Score.ToString();
teamTwoName.Text = "<bye>";
teamTwoScoreValue.Text = "0";
}
else
{
teamOneName.Text = "Not yet set.";
teamOneScoreValue.Text = "";
}
}
if (i == 1)
{
if (m.Entries[0].TeamCompeting != null)
{
teamTwoName.Text = m.Entries[1].TeamCompeting.TeamName;
teamTwoScoreValue.Text = m.Entries[1].Score.ToString();
}
else
{
teamTwoName.Text = "Not yet set.";
teamTwoScoreValue.Text = "";
}
}
}
}
private void matchupListBox_SelectedIndexChanged(object sender, EventArgs e)
{
LoadMatchup();
}
}
}
Oh by the way, probably important info I almost forgot, is that it errors on the second last method on line
MatchupModel m = (MatchupModel)matchupListBox.SelectedItem;
for (int i = 0; i < m.Entries.Count; i++)
because m is now null, although SelectedItem is 2 (int for a MatchUp model) on the form.
Thanks in advance kind people.
You can leave out the matchupListBox.DataSource = null;
Try out something like this
{
private TournamentModel tournament;
BindingList<int> rounds = new BindingList<int>();
BindingList<MatchupModel> selectedMatchups = new();
public TournamentViewerForm(TournamentModel tournamentModel)
{
InitializeComponent();
tournament = tournamentModel;
WireUpLists();
LoadFormData();
LoadRounds();
}
private void WireUpLists()
{
roundDropDown.DataSource = rounds;
matchupListBox.DataSource = selectedMatchups;
matchupListBox.DisplayMember = "DisplayName";
}
private void LoadFormData()
{
TournamentName.Text = tournament.TournamentName;
}
private void LoadRounds()
{
rounds.Clear();
rounds.Add(1);
int currRound = 1;
foreach (List<MatchupModel> matchups in tournament.Rounds)
{
if (matchups.First().MatchupRound > currRound)
{
currRound = matchups.First().MatchupRound;
rounds.Add(currRound);
}
}
LoadMatchups(1);
}
private void roundDropDown_SelectedIndexChanged(object sender, EventArgs e)
{
LoadMatchups((int)roundDropDown.SelectedItem);
}
private void LoadMatchups(int round)
{
foreach (List<MatchupModel> matchups in tournament.Rounds)
{
if (matchups.First().MatchupRound == round)
{
matchupListBox.SelectedIndexChanged -= matchupListBox_SelectedIndexChanged;
selectedMatchups.Clear();
matchupListBox.SelectedIndexChanged += matchupListBox_SelectedIndexChanged;
foreach (MatchupModel m in matchups)
{
selectedMatchups.Add(m);
}
}
}
if (selectedMatchups.Count > 0)
{
LoadMatchup(selectedMatchups.First());
}
}
private void LoadMatchup()
{
MatchupModel m = (MatchupModel)matchupListBox.SelectedItem;
for (int i = 0; i < m.Entries.Count; i++)
{
if (i == 0)
{
if (m.Entries[0].TeamCompeting != null)
{
teamOneName.Text = m.Entries[0].TeamCompeting.TeamName;
teamOneScoreValue.Text = m.Entries[0].Score.ToString();
teamTwoName.Text = "<bye>";
teamTwoScoreValue.Text = "0";
}
else
{
teamOneName.Text = "Not yet set.";
teamOneScoreValue.Text = "";
}
}
if (i == 1)
{
if (m.Entries[0].TeamCompeting != null)
{
teamTwoName.Text = m.Entries[1].TeamCompeting.TeamName;
teamTwoScoreValue.Text = m.Entries[1].Score.ToString();
}
else
{
teamTwoName.Text = "Not yet set.";
teamTwoScoreValue.Text = "";
}
}
}
}
private void matchupListBox_SelectedIndexChanged(object sender, EventArgs e)
{
LoadMatchup((MatchupModel)matchupListBox.SelectedItem);
}
}
I changed the selectedMatchups global variable type from List to that of BindingList
and then disabled the matchupListSelectedIndexChanged event just before i clear the selectedMatchups from the LoadMatchups function with selectedMatchups.Clear() and then re-eanabled after enable it just again.
This is so because this line of code selectedMatchups.Clear() triggers the matchListSelectedIndexChange event to run when not appropiate

Programmatically adding an object and selecting the correspondig row does not make it become the CurrentRow

I'm in a struggle with the DataGridView: I do have a BindingList of some simple objects that implement INotifyPropertyChanged. The DataGridView's datasource is set to this BindingList. Now I need to add an object to the list by hitting the "+" key. When an object is added, it should appear as a new row and it shall become the current row. As the CurrentRow-property is readonly, I iterate through all rows, check if its bound item is the newly created object, and if it is, I set this row to "Selected = true;"
The problem: although the new object and thereby a new row gets inserted and selected in the DataGridView, it still is not the CurrentRow! It does not become the CurrentRow unless I do a mouse click into this new row.
In this test program you can add new objects (and thereby rows) with the "+" key, and with the "i" key the data-bound object of the CurrentRow is shown in a MessageBox.
How can I make a newly added object become the CurrentObject? Thanks for your help!
Here's the sample:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
BindingList<item> myItems;
public Form1()
{
InitializeComponent();
myItems = new BindingList<item>();
for (int i = 1; i <= 10; i++)
{
myItems.Add(new item(i));
}
dataGridView1.DataSource = myItems;
}
public void Form1_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Add)
{
addItem();
}
}
public void addItem()
{
item i = new item(myItems.Count + 1);
myItems.Add(i);
foreach (DataGridViewRow dr in dataGridView1.Rows)
{
if (dr.DataBoundItem == i)
{
dr.Selected = true;
}
}
}
private void btAdd_Click(object sender, EventArgs e)
{
addItem();
}
private void dataGridView1_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Add)
{
addItem();
}
if (e.KeyCode == Keys.I)
{
MessageBox.Show(((item)dataGridView1.CurrentRow.DataBoundItem).title);
}
}
}
public class item : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int _id;
public int id {
get
{
return _id;
}
set
{
this.title = "This is item number " + value.ToString();
_id = value;
InvokePropertyChanged(new PropertyChangedEventArgs("id"));
}
}
private string _title;
public string title {
get
{
return _title;
}
set
{
_title = value;
InvokePropertyChanged(new PropertyChangedEventArgs("title"));
}
}
public item(int id)
{
this.id = id;
}
#region Implementation of INotifyPropertyChanged
public void InvokePropertyChanged(PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, e);
}
#endregion
}
}
I found the answer in this article: http://www.c-sharpcorner.com/Forums/Thread/43898/
Selecting the row is not enough, you have to set the CurrentCell to make the row become the CurrentRow. So to make the code above work, you have to change this:
public void addItem()
{
item i = new item(myItems.Count + 1);
myItems.Add(i);
foreach (DataGridViewRow dr in dataGridView1.Rows)
{
if (dr.DataBoundItem == i)
{
dr.Selected = true;
}
}
}
to that:
public void addItem()
{
item i = new item(myItems.Count + 1);
myItems.Add(i);
foreach (DataGridViewRow dr in dataGridView1.Rows)
{
if (dr.DataBoundItem == i)
{
dr.Selected = true;
dataGridView1.CurrentCell = dr.Cells[0];
}
}
}

Duplicate clicks on xx_Tap() happening in separate class

The scenario:
I want to tap on a textblock and run a method to add that item to the cart.No, I don't prefer buttons to textblocks, thank you :)
The code (shoppingcart.cs)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Threading.Tasks;
namespace m_POS
{
public class shoppingcart
{
int cartnum;
int duplicate=0;
int num_of_items;
int counter=1;
List<item> items = new List<item>();
//constructor
public shoppingcart()
{
this.cartnum = counter;
counter += 1;
this.num_of_items = 0;
this.items = new List<item>();
init_items();
}
//return the item list of tapped/purchased items
public List<item> getitems(){
return this.items;
}
//returns the number of items tapped/purchased
public int get_num_of_items() { return this.num_of_items; }
// the method that adds a tapped-on item to the items list
public void additem(String itemx,String qty) {
for (int i = 0; i < item.pick_item.Count; i++)
{
if (itemx.Equals(item.pick_item[i].getname()))
{
item itm = new item(item.pick_item[i].getname(),
item.pick_item[i].getprice());
itm.addqty(Convert.ToInt16(qty));
this.items.Add(itm);
Debug.WriteLine("added to cart!!");
}
}
this.num_of_items += Convert.ToInt16(qty);
}
//used to test the additem() works. Everytime the class is run, this Rolex item will
//be the first to be added to the cart. ALWAYS. Funny thing is, it doesnt get
// duplicated.
private void init_items()
{
item itm12 = new item("Rolex", 4000);
//additem(itm12);
this.items.Add(itm12);
}
}
////////////////////////////////////////////////////////////////////////////////////////
public class item
{
String itemname;
int itemamount;
int itemqty = 0;
public static List<item> pick_item = new List<item>();
public static List<String> menu_food = new List<string> {
"Single Beef Burger",
"Double Beef Burger",
"Triple Beef Burger",
"Single Chicken Burger",
"Double Chicken Burger",
"Single Veggie Burger",
"1/2 Fries",
"Full Fries",
"Beef Steak",
"Mushroom",
"Steamed Rice",
"Rolex"};
public static List<String> menu_price = new List<String>{
"8000",
"17000",
"25000",
"12000",
"26500",
"7500",
"4000",
"6000",
"20000",
"25000",
"17500",
"4000"};
public item(string name, int amount)
{
this.itemamount = amount;
this.itemname = name;
this.itemqty = 1;
}
public static void init_menu()
{
for (int i = 0; i < get_menu().Count; i++)
{
item itm = new item(menu_food[i], Convert.ToInt32(menu_price[i]));
pick_item.Add(itm);
}
}
public void addqty(int qty) { this.itemqty = qty; }
public string getname() { return this.itemname; }
public int getprice() { return this.itemamount; }
public static int getpxbyname(string itemname) {
int ans=0;
for (int y = 0; y < pick_item.Count; y++) {
if (pick_item[y].itemname.ToString().Equals(itemname)) {
ans = pick_item[y].itemamount;
}
}
return ans;
}
public static List<String> get_menu() { return menu_food; }
public static List<String> get_price() { return menu_price; }
}
}
Where are the duplicates happening?
I'm getting the additem(string itemname,int itemqty) being run twice on every tap. Everything else is perfect, though.
What have I done before posting?
- Tested the Tap event and made sure it was only being fired ONCE per tap? Check.
- Tested the additem() method to make sure it works to being with? Check. I add a single item to the cart everytime the app is started. That item never gets duplicated.
Console Debug.WriteLine() shows
added to cart!!
added to cart!!
called method with System.Windows.Controls.TextBlock
The first two added to carts are from the method being called twice.
The next called method with System.Windows.Controls.TextBlock is from the debug i inserted just after calling this method from the Food.xaml.cs
Food.xaml.cs [part of it]
public Foods()
{
InitializeComponent();
item.init_menu();
populatemenu();
}
public void populatemenu()
{
List<String> display = item.get_menu();
for (int i = 0; i < display.Count; i++)
{
string tname = "tb" + i;
TextBlock tb = new TextBlock();
tb.Tap += new EventHandler<System.Windows.Input.GestureEventArgs>(tb_Click);
tb.Style = (Style)Resources["textblocker"];
tb.FontSize = 36;
tb.Text = display[i];
tb.Name = tname;
sp_lunch.Children.Add(tb);
}
}
private void tb_Click(object sender, RoutedEventArgs e)
{
tapped += 1;
selectqty(sender);
}
private void selectqty(object sender) {
Popup popup = new Popup();
popup.Height = 300;
popup.Width = 400;
popup.VerticalOffset = 100;
PopUpControl control = new PopUpControl();
popup.Child = control;
popup.IsOpen = true;
string qty="";
control.btnOK.Click += (s, args) =>
{
popup.IsOpen = false;
//pick input from the popup's textbox.
qty = control.tbx.Text;
if (qty == null||qty=="") { qty = "0"; }
//send clicked item to cart for addition
TextBlock clicked = ((TextBlock)sender);
string temp = clicked.Text;
Cart.cart_new.additem(temp, qty);
Debug.WriteLine("called method with "+sender.ToString());
tb_pamount_lunch.Text = Convert.ToString(Cart.cart_new.get_num_of_items());
//tb_pamount_lunch.Text = tapped.ToString();
MessageBox.Show(temp);
//update the dinner stackpanel to display the selected items
sp_dinner.Children.Clear();
List<item> display = Cart.cart_new.getitems();
for (int i = 0; i < display.Count; i++)
{
TextBlock tb1 = new TextBlock();
tb1.FontSize = 36;
tb1.Text = display[i].getname().ToString();
sp_dinner.Children.Add(tb1);
}
};
control.btnCancel.Click += (s, args) =>
{
//close popup when cancel is clicked
popup.IsOpen = false;
};
}
Any more info??
If there's some other class you'd want to take a look at, I'll gladly copy/paste it here or upload the whole project.zip :)
I think when you tab the thrid time, 3 items will be added.
(s, args) =>
{
popup.IsOpen = false;
//pick input from the popup's textbox.
qty = control.tbx.Text;
if (qty == null||qty=="") { qty = "0"; }
//send clicked item to cart for addition
TextBlock clicked = ((TextBlock)sender);
string temp = clicked.Text;
Cart.cart_new.additem(temp, qty);
Debug.WriteLine("called method with "+sender.ToString());
tb_pamount_lunch.Text = Convert.ToString(Cart.cart_new.get_num_of_items());
//tb_pamount_lunch.Text = tapped.ToString();
MessageBox.Show(temp);
//update the dinner stackpanel to display the selected items
sp_dinner.Children.Clear();
List<item> display = Cart.cart_new.getitems();
for (int i = 0; i < display.Count; i++)
{
TextBlock tb1 = new TextBlock();
tb1.FontSize = 36;
tb1.Text = display[i].getname().ToString();
sp_dinner.Children.Add(tb1);
}
};
control.btnCancel.Click += (s, args) =>
{
//close popup when cancel is clicked
popup.IsOpen = false;
};
This is because the control still exists and every time you call selectqty another action is added to the eventhandler list. And will be executed more than ones. You should only register the event ones. Like in your constructor.
Personal: You should register events to a button that within a subcontrol. Instead create a new event on the control and raise it there. This will give you the advantage to change the buttons or other controls (maybee you want short-cut keys in future) on that controls without altering you mainform.
example:
public class Form1 : Form
{
public Form1()
{
InitializeComponent();
control.btnOKClicked += Control_buttonOk;
control.btnCancelClicked += Control_buttonCancel;
}
private void Control_buttonOk(object sender, eventArgs e)
{
// implement code
}
private void Control_buttonCancel(object sender, eventArgs e)
{
// implement code
}
}
public class UserControl1: UserControl
{
public UserControl1()
{
InitializeComponent();
btnOK.Click += (sender, e) =>
{
if(btnOKClicked != null)
btnOKClicked(this, EventArgs.Empty);
};
btnCancel.Click += (sender, e) =>
{
if(btnCancelClicked!= null)
btnCancelClicked(this, EventArgs.Empty);
};
}
public event EventHandler btnOKClicked;
public event EventHandler btnCancelClicked;
}
This way you separate the functionallity/dependency of the layout of the control.
As i was writing this, i think you might look over here:
Form.ShowDialog Method http://msdn.microsoft.com/en-us/library/c7ykbedk.aspx This will handle the popup.IsOpen and will block your MainForm until it's closed. With a DialogResult you can read if the user pressed Ok or cancel.

INotifyPropertyChanged and Threading

I have a base class implementing INotifyPropertyChanged:
protected void OnNotifyChanged(string pName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(pName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
I have a derived class with a property Latitude like so:
private double latitude;
public double Latitude
{
get { return latitude; }
set { latitude = value; OnNotifyChanged("Latitude"); }
}
My derived class also has a method Fly that manipulates Latitude.
I also have a Form with a TextBox bound to Latitude of my derived class:
txtLat.DataBindings.Clear();
txtLat.DataBindings.Add("Text", bindSrc, "Latitude");
A thread is used to kick off Fly like so:
Thread tFly = new Thread(f.Fly);
tFly.IsBackground = true;
tFly.Start();
When Latitude changes, an exception is thrown:
DataBinding cannot find a row in the list that is suitable for all bindings.
This seems to be an odd issue with thread affinity. Ultimately, the code is trying to do the update from a non-UI thread - I'm unclear why it isn't just displaying the cross-thread exception, though - I wonder whether this is actually a catch-all exception handler. If I remove the BindingSource (and bind directly to the object, which is valid) you do get a cross-thread exception (which I expected).
Personally, I would be inclined to handle this manually, i.e. subscribe to the event with a method that does an Invoke to the UI thread and updates the Text manually. However, I'm just checking if some previous cross-threaded binding code might help...
Here's an example using Invoke:
using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;
class FlightUav : INotifyPropertyChanged
{
protected void OnNotifyChanged(string pName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(pName));
}
public event PropertyChangedEventHandler PropertyChanged;
private double _latitude;
public double Latitude
{
get { return _latitude; }
set { _latitude = value; OnNotifyChanged("Latitude"); }
}
public void Fly()
{
for (int i = 0; i < 100; i++)
{
Latitude++;
Thread.Sleep(10);
}
}
[STAThread]
static void Main()
{
using (Form form = new Form())
{
FlightUav currentlyControlledFlightUav = new FlightUav();
currentlyControlledFlightUav.PropertyChanged += delegate
{ // this should be in a *regular* method so that you can -= it when changing bindings...
form.Invoke((MethodInvoker)delegate
{
form.Text = currentlyControlledFlightUav.Latitude.ToString();
});
};
using (Button btn = new Button())
{
btn.Text = "Fly";
btn.Click += delegate
{
Thread tFly = new Thread(currentlyControlledFlightUav.Fly);
tFly.IsBackground = true;
tFly.Start();
};
form.Controls.Add(btn);
Application.Run(form);
}
}
}
}
Here's an example using a (modified) version of some old threading code of mine:
using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;
class FlightUav : INotifyPropertyChanged
{
protected void OnNotifyChanged(string pName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(pName));
}
public event PropertyChangedEventHandler PropertyChanged;
private double _latitude;
public double Latitude
{
get { return _latitude; }
set { _latitude = value; OnNotifyChanged("Latitude"); }
}
public void Fly()
{
for (int i = 0; i < 100; i++)
{
Latitude++;
Thread.Sleep(10);
}
}
[STAThread]
static void Main()
{
using (Form form = new Form())
{
FlightUav currentlyControlledFlightUav = new FlightUav();
BindingSource bindSrc = new BindingSource();
var list = new ThreadedBindingList<FlightUav>();
list.Add(currentlyControlledFlightUav);
bindSrc.DataSource = list;
form.DataBindings.Clear();
form.DataBindings.Add("Text", list, "Latitude");
using (Button btn = new Button())
{
btn.Text = "Fly";
btn.Click += delegate
{
Thread tFly = new Thread(currentlyControlledFlightUav.Fly);
tFly.IsBackground = true;
tFly.Start();
};
form.Controls.Add(btn);
Application.Run(form);
}
}
}
}
public class ThreadedBindingList<T> : BindingList<T>
{
private readonly SynchronizationContext ctx;
public ThreadedBindingList()
{
ctx = SynchronizationContext.Current;
}
protected override void OnAddingNew(AddingNewEventArgs e)
{
SynchronizationContext ctx = SynchronizationContext.Current;
if (ctx == null)
{
BaseAddingNew(e);
}
else
{
ctx.Send(delegate
{
BaseAddingNew(e);
}, null);
}
}
void BaseAddingNew(AddingNewEventArgs e)
{
base.OnAddingNew(e);
}
protected override void OnListChanged(ListChangedEventArgs e)
{
if (ctx == null)
{
BaseListChanged(e);
}
else
{
ctx.Send(delegate
{
BaseListChanged(e);
}, null);
}
}
void BaseListChanged(ListChangedEventArgs e)
{
base.OnListChanged(e);
}
}

Categories