UITableView with Event Handler - c#

So in my app I'm using a UITableView with a custom UITableViewCell to display a list of 'CustomItems' that contains a UISegmentedControl as an On/Off switch. Changing the value of a UISegmentedControl in cell 2, should turn on/off CustomItem[2], etc.
The UISegmentedControl performs an action for a CustomItem when the value is changed in a worker thread. The code I have works fine 99% of the time - in fact I have never seen it misbehaving. However we have customers who have reported that the UISegemented control is SOMETIMES not controlling the correct item.
In my UITableViewSource 'GetCell()' method, I have code that looks similar to this:
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
var cell = tableView.DequeueReusableCell(cellIdentifier) as CustomCell ?? new CustomCell(cellIdentifier);
. . .
// Handle the UISegementedControl (On/Off switch) value changed event
cell.segmentControl.ValueChanged += delegate
{
// Do some stuff
cell.outputControl.TintColor = cell.outputControl.SelectedSegment == 0 ? UIColor.Gray : UIColor.Red;
var currentItem = tableItems[cell.IndexPath.Row];
TurnSomethingOnOrOff(currentItem);
}
}
private void TurnSomethingOnOrOff(CustomItem currentItem)
{
var worker = new BackgroundWorker();
worker.DoWork += (sender, args) => {
// turn currentItem on or off
};
worker.RunWorkerAsync ();
}
What the user is seeing is that, on occasion, changing the value of the UISegementedControl in cell 1, is turning both CustomItem[0] and CustomItem[1] on or off.
I'm not sure how to get my app into the state that my customer is able to (they're not entirely sure either, it seems to happen at random).
Could this be something to do with Garbage Collection or some other reason why my cells would be losing the correct Event Handler?
The list only has 2 or 3 items in it, so cells generally aren't being reused. I did test the cell reuse (by scrolling off screen so the cells have to be redisplayed), and all that happened was the ValueChanged event is triggered twice, but the wrong event was never triggered.

Related

Wpf application and Threads

I have problem with my GUI and Threads.
The GUI contains DataGrid. Every X time the program do some query and getting a list of items that I want to fill into the DataGrid.
So far so good:
private void loadTaskList() //Call every X time
{
List<myObject> myList = myquery();
this.Dispatcher.Invoke((Action)(() =>
{
TaskListTable.Items.Clear(); //Clear the DataGrid
foreach (myObject O in myList) //Add the items from the new query.
{
TaskListTable.Items.Add(O);
}
}));
FindSelectionObject(); // <-- see next explanation.
}
When the user click on one of the objects in the datagrid, the line color changed (it works fine), but when the program reload the table,The painted line disappears (Becuse I clear and add new objects).
To deal with it, I created the function FindSelectionObject():
private void FindSelectionObject()
{
this.Dispatcher.Invoke((Action)(() =>
{
this.SelectedIndex = TaskListTable.Items.IndexOf((myObject)lastSelectionObject); //find index of the new object that equels to the last selection object.
var row = TaskListTable.ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as DataGridRow; //get the row with the index
row.Background = Brushes.LightGoldenrodYellow; //repaint
}));
}
The problem: Everything works fine, but sometimes when the program reloads, the line flashes per second and then highlighted back, and sometimes it's not painting it at all (untill the next reload).
I can't understand why this is happening. I think maybe the FindSelectionObject() begins to run before the loadTaskList() ends to invoke all and add the new objects into the datagrid.
But if so - Why? And how can I fix it?
In the bottom line, I want that after every reload the line re-paint immediately..
Thanks for any advice!
A few things to think about:
You should keep in mind that the DataGrid uses virtualization, which means that each item in your items source does not get its very own UI element. The UI elements are created to fill the visible area, and then re-used depending on which data-source item is currently bound to each one (this changes when you scroll for instance or change the items source). This may cause you problems in the future if you use your current approach, so keep this in mind.
The other thing is that the DataGrid may require more "cycles" of the layout process in order to update its UI. You may simply be calling FindSelectionObject prematurely. You have queued FindSelectionObject right after the invocation in loadTaskList. If the DataGrid needs to perform some actions which are queued on the dispatcher after the items source has changed, these will execute after the invocation in FindSelectionObject.
Try this instead:
private void loadTaskList() //Call every X time
{
List<myObject> myList = myquery();
this.Dispatcher.Invoke((Action)(() =>
{
TaskListTable.Items.Clear(); //Clear the DataGrid
foreach (myObject O in myList) //Add the items from the new query.
{
TaskListTable.Items.Add(O);
}
// The items of the grid have changed, NOW we QUEUE the FindSelectionObject
// operation on the dispatcher.
FindSelectionObject(); // <-- (( MOVE IT HERE )) !!
}));
}
EDIT: OK, so if this fails then maybe this will cover the case in which the above solution fails: subscribe to the LoadingRow event of DataGrid and set the appropriate background color if the row is the selected one. So in the cases when new rows are created this event will be called (due to virtualization it is not called per item in items source, but per actual row UI element). In the event args you will have access to the created DataGridRow instance.
I think this issue could be a visual thread synchronization. For this you can create and use a method similar like this:
public void LockAndDoInBackground(Action action, string text, Action beforeVisualAction = null, Action afterVisualAction = null)
{
var currentSyncContext = SynchronizationContext.Current;
var backgroundWorker = new BackgroundWorker();
backgroundWorker.DoWork += (_, __) =>
{
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US");
Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-US");
currentSyncContext.Send((t) =>
{
IsBusy = true;
BusyText = string.IsNullOrEmpty(text) ? "Espere por favor..." : text;
if (beforeVisualAction != null)
beforeVisualAction();
}, null);
action();
currentSyncContext.Send((t) =>
{
IsBusy = false;
BusyText = "";
if (afterVisualAction != null)
afterVisualAction();
}, null);
};
backgroundWorker.RunWorkerAsync();
}
IsBusy and BusyText are particular properties, that you can remove. The action variable will be the action to do in background (load your items for instance). beforeVisualAction and afterVisualAction are the visual actions you may want to do before and after the background action. Here are any visual update, for instance select your item, change color, set a view model variable that raise a binding update,... (any action that update the view).
Hope this method helps.
Are you maintaining the reference to lastSelectionObject somewhere? You say you're adding new objects, if they are truly new then the reference will be different and the reference comparison happening in IndexOf will not find it.

WPF C# How to avoid event fire whle importing?

I have a WPF app with a Window and different UserControls are shown in it one by one no button clicks.
I import data from a file and all data is stored in a common object "ImportExportData". All UserControls are bind to respective Property (as custom objects like Data1, Data2...) of ImportExportData class.
In my USerControl I have combobox's for NumberZones proeprty those SelectionChanged event is handled respectively. In the SelectionChanged event of this combobox, based on the number selected that many rows are added to an ObservableCollection of Data2 property.
While importing data and setting the imported object (Data2) as the DataContext of USerControl2, it sets the NumberZones property value to the respective combobox and SelectionChanged event is fired as it should. At this time, the object already contains reqd rows in ObservableCollection and this event should not add it.
PArent window has a flag "importedData" that tells me that the object is imported. But I can't make that false once UserContrl2 is loaded, as their are their UC that will follow UC2. In UC2 I can create another flag "importing" and make it false once all UI is loaded. Thru which UC event can I know that UI is loaded and thus make "importing" as false ??
I am wondering how do I avoid from firing the SelectionChanged event when the imported object is populating the UI components. Which event of the UserControl will help me in this case maybe to keep a flag in USerControl2.
Any idea, suggestions please.
It is very hard to understand all of your question, so bear with me... I'll address each point that I understand.
Thru which UC event can I know that UI is loaded and thus make "importing" as false ??
Take a look at the FrameworkElement.Loaded Event page at MSDN.
I am wondering how do I avoid from firing the SelectionChanged event when the imported object is populating the UI components.
There are two way of achieving this goal... The first way does not stop the event from firing, but instead ignores it when data is being imported. basically involves temporarily unsubscribing from the SelectionChanged event and then re-subscribing to it. If I understand you correctly, you have a bool property in your parent Window and SelectionChanged handlers in your UserControls... first, you can add a bool property to each of your UserControls:
public bool CanChangeSelection { get; set; }
Now, in your parent Window (assuming that you have references to your controls) you can update your property:
private bool isImporting = false;
public bool IsImporting
{
get { return isImporting; }
set
{
isImporting = value;
UserControl1.CanChangeSelection = isImporting;
UserControl2.CanChangeSelection = isImporting;
...
UserControlN.CanChangeSelection = isImporting;
}
}
Then finally, in your control SelectionChanged handlers:
private void SelectionChangedHandler(object sender, RoutedEventArgs e)
{
if (CanChangeSelection)
{
// do your stuff in here
}
}
The second way basically involves temporarily unsubscribing from the SelectionChanged event and then re-subscribing to it. For this option, we need to change the definition of our new bool property in each of your UserControls:
private bool canChangeSelection = false;
public bool CanChangeSelection
{
get { return canChangeSelection; }
set
{
canChangeSelection = value;
if (!canChangeSelection)
{
if (SelectionChangedHandler != null) ComboBox1.SelectionChanged -=
SelectionChangedHandler;
}
else if (SelectionChangedHandler == null) ComboBox1.SelectionChanged +=
SelectionChangedHandler;
}
}
I personally prefer the first method as it is more straightforward.

FlyoutNavigation Controller

Using MonoDevelop, I have been looking at an IOS implementation of a side slide out menu using FlyoutNavigationController, but have hit a couple of stumbling blocks.
Firstly, how can you access the font elements of the generated list?
I can easily modify row heights etc, but am unsure of how to proceed with modifying the list items, can this be down with a tablesource and item styling?
Secondly, how to open a view from this list?
Currently an empty view is used by default but new views are to be opened from the side menu list, I have tried using the push navigation controller but it fails to open.
Any ideas are more than welcome.
navigation = new FlyoutNavigationController();
navigation.View.Frame = UIScreen.MainScreen.Bounds;
View.AddSubview(navigation.View);
navigation.NavigationRoot = new RootElement ("Menu List")
{
new Section ("Menu List")
{
from page in SlideList
select new StringElement (page.title) as Element
}
};
navigation.NavigationTableView.BackgroundColor = UIColor.DarkGray;
navigation.NavigationTableView.RowHeight = 30;
navigation.NavigationTableView.SeparatorStyle = UITableViewCellSeparatorStyle.SingleLine;
navigation.NavigationTableView.SeparatorColor = UIColor.LightGray;
navigation.NavigationTableView.SectionHeaderHeight = 60;
//navigation.NavigationTableView.DataSource = SlideList;
//navigation.ViewControllers = Array.ConvertAll (MenuItems, title => new UINavigationController (new TaskPageController (navigation, title)));
navigation.ViewControllers = Array.ConvertAll (MenuItems, title => new TaskPageController (navigation, title));
this.NavigationItem.LeftBarButtonItem = new UIBarButtonItem (UIBarButtonSystemItem.Action, delegate {
navigation.ToggleMenu();
});
I haven't used the FlyOutNavigationController before, but I took a look at this example:
https://github.com/xamarin/FlyOutNavigation
It looks like you're supposed to have the same number of StringElements as Controllers. For the ViewControllers array, it looks like you can supply your own custom controllers instead of just plain ViewControllers. After that, clicking a list item should automatically navigate to the appropriate controller.
In regards to styling, looking at the source for this NavigationController, I don't see much in terms of being able to stylize the cells. I did a quick search for how to go about styling MonoTouch Dialog lists and it looks like there isn't an easy way without subclassing elements:
Monotouch Dialog: Styling Elements
However, I can share with you how I've accomplished the two questions you asked without the Dialog framework.
You can create a custom class that extends UITableViewSource:
http://docs.xamarin.com/guides/ios/user_interface/tables/part_2_-_populating_a_table_with_data
In the GetCell method override, you can grab an instance of the cell's label and set the font like so:
cell.TextLabel.Font = UIFont.FromName("TitlingGothicFB Cond", 20);
Another thing you can do with your custom UITableViewSource class is create a custom event:
public event EventHandler ListItemSelected;
Inside the RowSelected method you can dispatch this event:
public override void RowSelected (UITableView tableView, MonoTouch.Foundation.NSIndexPath indexPath)
{
ListItemSelected(this, new MyCustomEventArgs(indexPath.Row));
}
In the controller class that was responsible for instantiating this TableSource, you can listen and handle this event like so:
var customTableSource = new CustomTableSource(myList);
MyTable.Source = customTableSource;
customTableSource.ListItemSelected += (object sender, EventArgs e) => {
if((e as MyCustomEventArgs).rowSelected == 1){
this.NavigationController.PushViewController(new MyNextViewController(), true));
}
}

ListView scroll control - scroll to bottom if user isn't scrolling?

I have a .NET 3.5 WinForm that has a ListView with the View set in Details mode. It functions as a scrollable list of status items on a long background task. I have the most recent ListViewItem (status entry) added to the bottom. To assure that it is seen, I ensure the visibility of the new item after adding. This all works fine; the list view automatically scrolls to the bottom to show the most recent item.
private void AddListItem(DateTime timestamp, string message, int index)
{
var listItem = new ListViewItem(timestamp.ToString());
listItem.SubItems.Add(message);
statusList.Items.Insert(index, listItem);
statusList.Items[statusList.Items.Count - 1].EnsureVisible();
}
The problem is if the user is scrolling up to look at older messages, the ListView will be scrolled down to make the new item visible as it comes in. Is there a way to control this behavior to check if the user is interacting with the scrollbar (specifically, if they're holding down the mouse button on the scrollbar)? It is probably also acceptable to detected if the scroll is always at the bottom. if it is not at the bottom, then I would not ensure the visibility of the latest item. Something like:
private void AddListItem(DateTime timestamp, string message, int index)
{
var listItem = new ListViewItem(timestamp.ToString());
listItem.SubItems.Add(message);
statusList.Items.Insert(index, listItem);
if (!statusList.IsScrollbarUserControlled)
{
statusList.Items[statusList.Items.Count - 1].EnsureVisible();
}
}
What's strange is that when the user is holding down the scrollbar "handle" in place, the handle doesn't move (implying that the view is not actually being scrolled down programatically), but in infact is.
Update: Is it possible to detect the position of the scrollbar, i.e., if i'ts at the bottom or not?
Two steps to solving this problem:
The WinForms ListView doesn't have a Scrolled event. We'll need to define one.
Determining when the ListView is idle, and calling EnsureVisible only when it's been idle for awhile.
For the first problem, inherit a new class from ListView, override the Windows message pump, and raise an event when the user scrolls it:
public class MyListView : ListView
{
public event EventHandler<EventArgs> Scrolled;
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
const int wm_vscroll = 0x115;
if (m.Msg == wm_vscroll && Scrolled != null)
{
Scrolled(this, new EventArgs());
}
}
}
Now we know when the user scrolls the list view. Your next problem is to determine whether the list view is idle; that is, if the user hasn't scrolled it in awhile.
There are multiple ways to do that. For this purpose, I'm just going to use a time stamp to indicate the last scroll time:
private DateTime lastScrollTime;
...
listView.Scrolled += delegate { lastScrollTime = DateTime.Now };
...
private void AddListItem(DateTime timestamp, string message, int index)
{
var listItem = new ListViewItem(timestamp.ToString());
listItem.SubItems.Add(message);
statusList.Items.Insert(index, listItem);
// Scroll down only if the list view is idle.
var idleTime = TimeSpan.FromSeconds(5);
var isListViewIdle = DateTime.Now.Subtract(this.lastScrollTime) > idleTime;
if (isListViewIdle)
{
statusList.Items[statusList.Items.Count - 1].EnsureVisible();
}
}
Compare to, say, SysInternals' ProcMon. Add a checkbox labeled "Auto scroll" so the user can turn it off.

WinForms DataGridView - Full text display and dataSource update

Earlier today i was suggested in here to use a DataGridView to print messages that needed a individual mark as read.
I followed the suggestion, and with some reading online i managed to bind it to my message list with the following results after some tweaking.
alt text http://img237.imageshack.us/img237/3015/datagridview.jpg
Currently i have 2 issues, the first one is that i didn't find a way to resize the row height to display the full message, and the second one is that when the list is updated, the DataGridView doesn't display the modifications.
Any way to solve both problems? Or do i need to use something other than DataGridView, and in that case what should i be using?
Also, is there any way to urls contained in the message to become clickable and be opened in the default browser?
EDIT
More info in relation to the binding.
Basically i have a class variable inside the form, and i do the initial binding with a button.
private void button1_Click(object sender, EventArgs e)
{
list.Add(new Class1() { Message = "http://www.google.com/", Read = false });
list.Add(new Class1() { Message = "Message way too long to fit in this small column width", Read = false });
dataGridView1.DataSource = list;
}
I then have another button that adds some more entries just to test it, and i know the list is properly updated, but there are no changes in the dataGridView.
EDIT 2
If i wasn't clear before i need for the width to be fixed, and the cell height that contains the long text to be enlarged and display the text in 2 lines
have you checked the options in the EditColumn using smart tag ?
you can add column of type
DataGridViewLinkColumn, set its Text property to Message
Try removing any value from width
and height properties for a
column. In this way, it will set the
column size (cell) size according to
the data size.
hope this helps
I'll take a stab and see if I can help.
First off the row height. There are two DataGridView Methods called AutoResizeRow and AutoResizeRows which will adjust the height of the row to fit the contents.
Can you show us how you are binding your data to the DataViewGrid and how the data might be modified? That will help with the modifications not updating.
As for the link, unfortunately I can't seem to find an object which handles this sort of thing natively. Most likely you will first have to decide if the text going into the DataGridView is a link (using a Regular Expression, if you were me). Second, display it differently in the DataGridView (underline it, make it blue). Third, put a click event on it and when that cell is clicked handle that by throwing it out to a browser. I will look a little further into it though since this seems like a lot of work (and I will keep my fingers crossed that someone knows better than I do).
Regarding the list not updating; there are two issues;
To notice add/remove, you need list binding events. The easiest way to do this is to ensure you use a BindingList<YourClass> rather than a List<YourClass>.
To notice changes to individual properties (in this context) you'll need to implement INotifyPropertyChanged on your type:
public class YourClass : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
private string message;
public string Message
{
get { return message; }
set { message = value; OnPropertyChanged("Message"); }
}
public bool isRead;
[DisplayName("Read")]
public bool IsRead
{
get { return isRead; }
set { isRead = value; OnPropertyChanged("IsRead"); }
}
}
For an example showing binding that to a list:
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
BindingList<YourClass> list = new BindingList<YourClass>();
DataGridView grid = new DataGridView();
grid.Dock = DockStyle.Fill;
grid.DataSource = list;
Button add = new Button();
add.Text = "Add";
add.Dock = DockStyle.Bottom;
add.Click += delegate
{
YourClass newObj = new YourClass();
newObj.Message = DateTime.Now.ToShortTimeString();
list.Add(newObj);
};
Button edit = new Button();
edit.Text = "Edit";
edit.Dock = DockStyle.Bottom;
edit.Click += delegate
{
if (list.Count > 0)
{
list[0].Message = "Boo!";
list[0].IsRead = !list[0].IsRead;
}
};
Form form = new Form();
form.Controls.Add(grid);
form.Controls.Add(add);
form.Controls.Add(edit);
Application.Run(form);
}

Categories

Resources