WPF Frame.Navigate triggering multiple events - c#

I'm a fairly new to WPF and C#.
I have a frame component on my main window and 4 buttons next to it that navigate to different views in the frame. Within one the the views there is a DataGrid that has a SelectionChanged event which makes an SQL call to a database that fetches records, whose data is then used to populate a list of custom objects (these relate to the selected item on the DataGrid).
Anyways, the problem I have is that from time to time multiple calls (2 or 3) to the SelectionChanged event are being triggered at the same time for a single selection change (mouseclick) on the DataGrid.
The navigation button click events on the main window all look like this:
private void btn_MyDesk_Click(object sender, RoutedEventArgs e)
{
MainFrame.Navigate(new Uri("/Views/MyDeskView.xaml", UriKind.Relative));
}
private void btn_AllOrders_Click(object sender, RoutedEventArgs e)
{
MainFrame.Navigate(new Uri("/Views/AllOrdersView.xaml", UriKind.Relative));
}
After some experiementation, I've found that the bug only happens after changing views away from the view with the DataGrid, and then changing back to it (but not always). When the bug appears the number of calls generally corresponds to the number of times I had switched views. Furthermore, the bug will simply vanish if leave the program alone for a minute or two. This makes me suspect that there multiple instances of the DataGrid view lingering like ghosts in memory and duplicating event calls until they are cleaned up by a garbage collector.
Should I be cleaning something up each time I switch views, or am I looking in the wrong place?
Thank you in advance for any help.
Edit: In answer to #Peter Moore
I subscribe to the event in the DataGrid declaration within the views XAML: SelectionChanged="dtg_MyDeskOrderGrid_SelectionChanged"
Edit: This is the sequence that happens on a selection change in the data grid. It includes several UI changes while the SQL records for the new selection are retrieved and displayed on a second DataGrid (dtg_MyDeskOrderItems). While the SQL call is being made, the relevant controls are disbaled and a semi-transparent panel (bdr_DGLoadingPanel) is moved on screen to cover them and display a loading animation. When the work is done, the work area is re-enabled and the loading panel moved off screen. Focus is also returned to the main "order" Datagrid.
dtg_MyDeskOrderGrid: This is the main DataGrid showing all "Orders"
dtg_MyDeskOrderItems: This is a secondary DataGrid that is updated to show all "Items" in the selected order.
private void dtg_MyDeskOrderGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
CurrSelectedOrder = (Order_class)dtg_MyDeskOrderGrid.SelectedItem;
if (CurrSelectedOrder.ItemList == null)
{
if (NowWorking == false)
{
NowWorking = true;
bdr_DGLoadingPanel.Margin = new Thickness(2);
dtg_MyDeskOrderGrid.IsEnabled = false;
bdr_FilterPanel.IsEnabled = false;
bdr_DGLoadingPanel.Focus();
img_LoadingCircle.RenderTransform = rt;
img_LoadingCircle.RenderTransformOrigin = new Point(0.5, 0.5);
da.RepeatBehavior = RepeatBehavior.Forever;
rt.BeginAnimation(RotateTransform.AngleProperty, da);
bdr_DGLoadingPanel.UpdateLayout();
worker.RunWorkerAsync();
}
}
else
{
dtg_MyDeskOrderItems.ItemsSource = null;
dtg_MyDeskOrderItems.ItemsSource = CurrSelectedOrder.ItemList;
dtg_MyDeskOrderItems.Items.Refresh();
}
}
private void worker_DoWork(object? sender, DoWorkEventArgs e)
{
DatabaseConnection DBConn9 = new DatabaseConnection();
DBConn9.FillOrderItems(CurrSelectedOrder);
}
private void worker_RunWorkerCompleted(object? sender, RunWorkerCompletedEventArgs e)
{
this.Dispatcher.Invoke(() =>
{
dtg_MyDeskOrderItems.ItemsSource = null;
dtg_MyDeskOrderItems.ItemsSource = CurrSelectedOrder.ItemList;
dtg_MyDeskOrderItems.Items.Refresh();
bdr_DGLoadingPanel.Margin = new Thickness(1000, 2, 2, 2);
rt.BeginAnimation(RotateTransform.AngleProperty, null);
dtg_MyDeskOrderGrid.IsEnabled = true;
bdr_FilterPanel.IsEnabled = true;
// The following work-around and accompanying GetDataGridCell function were used to give keyboard focus back to the datagrid to make navigation with arrow keys work again.
// It appears keyboard focus is not returned to the Datagrid cells when using the Datagrid.focus() method.
Keyboard.Focus(GetDataGridCell(dtg_MyDeskOrderGrid.SelectedCells[0]));
NowWorking = false;
});
}
Edit...
Following the advice of the commentors, I was able to fix the bug by unsubscribing from the event in the Unloaded event for the view containing the DataGrid:
private void uct_MyDeskView_Unloaded(object sender, RoutedEventArgs e)
{
dtg_MyDeskOrderGrid.SelectionChanged -= dtg_MyDeskOrderGrid_SelectionChanged;
}
However, I was not able to reproduce the bug using a barebones testing project.
The UI in the original project is quite heavy, so I'm wondering if it's not the old view and events lingering in memory as this seems to fit the behavior of the bug & fix (Only occuring when I navigate away and back causing a new view to be created, multiple event triggers corresponding to the number of times I navigated away, and then finally the bug vanishing of its own accord after a moment or two).
I won't be settling on this as a final solution and instead will learn about ways I can reuse instances of my views (as suggested by Bionic) instead of recreating them. The reason for this is, if the SelectionChanged event is getting multiple triggers from old view instances, then it is likely other events will suffer from the same bug. This would be bad.
#BionicCode If you are still around, could you repost your initial comment as a solution so I can mark it answered?
Thank you to everyone for all the help and education. ^_^

Following the advice of the commentors, I was able to initially fix the bug by unsubscribing from the event in the Unloaded event for the view containing the DataGrid:
private void uct_MyDeskView_Unloaded(object sender, RoutedEventArgs e)
{
dtg_MyDeskOrderGrid.SelectionChanged -= dtg_MyDeskOrderGrid_SelectionChanged;
}
However as this solution only disconnected the one event and didn't solve the root problem of old views lingering in the background and firing events before being cleaned up, I finally went the following code that keeps only one instance of my view in memory and reuses it.
private MyDeskView? currMyDeskView = null;
private void btn_MyDesk_Click(object sender, RoutedEventArgs e)
{
if (currMyDeskView == null)
{
currMyDeskView = new MyDeskView();
}
MainFrame.Navigate(currMyDeskView);
}
Thank you to everyone who helped me get over this bug.

Related

UWP CustomRenderer for Checkbox: Pointer over Checkbox changes style?

I'm working with Xamarin.Forms and I made a CustomRenderer for Checkbox in UWP. When I set all the Checkboxes of my items in the ListView to true by clicking the button "Alle", the Checkboxes are displayed correctly with the check inside the box:
However, if I hover my mouse over the Checkboxes, they immediately change their appearence (the check disappears but it's still selected). In the following screenshot, I moved my cursor over the 3rd - 7th Checkboxes:
This is my overridden OnElementChanged method in the CustomRenderer:
protected override void OnElementChanged(ElementChangedEventArgs<EvaCheckbox> e)
{
base.OnElementChanged(e);
var model = e.NewElement;
if (model == null)
{
return;
}
nativeCheckbox = new CheckBox();
CheckboxPropertyChanged(model, null);
model.PropertyChanged += OnElementPropertyChanged;
nativeCheckbox.Checked += (object sender, Windows.UI.Xaml.RoutedEventArgs eargs) =>
{
model.IsChecked = (bool)nativeCheckbox.IsChecked;
};
nativeCheckbox.Unchecked += (object sender, Windows.UI.Xaml.RoutedEventArgs eargs) =>
{
model.IsChecked = (bool)nativeCheckbox.IsChecked;
};
SetNativeControl(nativeCheckbox);
}
I tried to override the PointerEntered event of nativeCheckbox. It works, for example if I set the model.IsChecked to true on this event, it will be set to true:
nativeCheckbox.PointerEntered += (s, args) =>
{
model.IsChecked = true;
};
But I don't know how to (if even at this place) prevent the checkbox from changing it's appearance when moving the cursor above the Checkbox. Just leaving the triggered event with empty code like this won't change anything about the described behaviour:
nativeCheckbox.PointerEntered += (s, args) => { };
How can I prevent the Checkbox from changing it's appearance when I move my cursor over it?
Update:
I've created a sample project for this issue. You can find the repository here: https://github.com/Zure1/CustomCheckbox
It has the exact same described behavior. In the following screenshot I pressed the button "All" on the bottom of the screen and then the checkboxes look like correct with a check inside of them:
After moving the mouse cursor over the bottom 3 checkboxes, their change their appearance:
Information: I'm debugging on my desktop (Windows 10). I don't know if this issue exists on WinPhone. Just in case you're wondering why my checkboxes are red: My system color in Windows is red.
This is a tricky one as I have been struggling with this issue for a while, I'll try my best to answer this.
TL;DR: It's caused by ViewCell.
The issue comes down to Xamarin Forms ListView and ViewCell.
I haven't been able to track down the cause yet for many months and the way I get around this issue is by refreshing the ListView every time a change happens forcing a redraw of the entire ListView which can really impact performance.
My educated guess on what the cause could be is the rendering code for the ViewCell is missing something.
As for your particular issue, I have created a CheckBoxCell which you can use to display a list of checkboxes with a title. I forked your project and made the changes.
This will display something similar to what you are trying to achieve and doesn't have rendering issues so will be a good starting point. You are able to customize this to display images and the like but you'll have to do that in the platform-specific layout code.
Please note that I have only created the code for UWP and that should be enough to get you going for the other platforms.
I hope this helps somewhat.

Does Grid.Children.Clear() really remove all controls?

Edit:
Simple answer: Yes, it does. I found the error, which was, that another event handler was added, everytime the Combobox_SelectionChanged was fired. Hence, the collection looked fine, but the Items_CollectionChanged was fired multiple times. Once this was fixed, everything worked fine.
End Edit.
I've got a page with a combobox and a grid. The grid fills dynamically, when the selection in the combobox changes. I'm now observing a strange thing. When I select a value for the second time in the combobox, the childitems in the grid appear twice. I've checked the underlying collections, which look fine (i.e. only one record per item). When I jump out of the combobox_SelectionChanged method, after the Grid.Children.Clear() command, the screen looks fine, i.e. empty.
My guess is, that Grid.Children.Clear() only removes the controls from the visual tree, but the actual controls are still hanging around. Any Ideas?
private void combobox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
grItems.Children.Clear();
grItemsColumnDefinitions.Clear();
grItemsColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(200) });
}
private void Items_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
grItems.Children.Add(new ChildItemControl(e.NewItems[0]));
}
}
Edit: The whole thing is supposed to look like this (fictional - but hopefully understandable - example)
I would suggest you use the built in Databinding for WPF. You could use something like this:
<DataGrid x:Name="grItems" ItemsSource="{Binding comboboxItems}" />
Then, when you update comboboxItems your grid will automatically update too!
Here is a great article on Databinding with the DataGrid control:
http://www.wpftutorial.net/DataGrid.html
For more information about Databinding in general, here is a good article: https://msdn.microsoft.com/en-us/library/aa480224.aspx

Restoring the original values in data context when cancelling the edit dialog

In my grid, upon a click, I'm providing the entire row's item to another window (modal dialog) and make it its data context. When the user is done with it, the update to the DB is made only if the dialog's result is true.
private void DataGridRow_OnClick(Object sender, MouseButtonEventArgs eventArgs)
{
Editor dialog = new Editor((sender as DataGridRow).Item as Thing);
if (dialog.DialogResult ?? false)
(DataContext as ViewModel).Update();
}
This works like a charm and only updates the DB if the user clicks the accepting button. However, it also has a certain unexpected behavior. The changes made in the editor window, although it's cancelled (rendering the value of DialogResult to be false, hence omitting the update of the DB), still remain visible in the grid.
It's kind of obvious once one thinks about it because we're binding to an object (call by reference) while feeding the editee object to the constructor of the modal window. But it's hardly anything that meets the users' expectations.
I'm about to code in a workaround creating a copy of the originally sent in object and then restoring it, if the update to the DB isn't carried out.
private void DataGridRow_OnClick(Object sender, MouseButtonEventArgs eventArgs)
{
Thing thing = (sender as DataGridRow).Item as Thing;
Thing clone = thing.GetClone();
Editor dialog = new Editor(thing);
if (dialog.DialogResult ?? false)
(DataContext as ViewModel).Update();
else
(sender as DataGridRow).Item = clone;
}
However, it's kind of inconvenient to keep extension methods for creating a copy (IClonable is deprecated) and all the shuffling back and forth smells a little. Is there a neater approach to resolve it?
The only other approach I can think of (and we're talking a lazy man's solution here) is to simply recreate the original data context, hence refreshing everything from the DB like so.
private void DataGridRow_OnClick(Object sender, MouseButtonEventArgs eventArgs)
{
Editor dialog = new Editor((sender as DataGridRow).Item as Thing);
if (dialog.DialogResult ?? false)
(DataContext as ViewModel).Update();
else
DataContext = new ViewModel();
}
Thoughts?
I have seen many different solutions for this from expert tutorials.
One is to copy the original values into a basic struct, which you can use to revert to by copying values from.
One is to copy the values from the model POCO object into ViewModel properties and only apply the changes when confirmed.
There are many ways to achieve the same result. Its just a matter of preference.

Second TextBox showing same Text Selection as first

Long time listener, first time caller here. I'm having a strange issue with the TextBox in WinRT C#/XAML that I hope someone may be able to help me with.
Basically, I'm working on creating a Custom Control that essentially requires a second TextBox to be a copy of the first, including showing the same Text, and showing the same Selected Text. Obviously for the Text requirement I simply respond to the TextChanged event on the first TextBox and set the Text of the second TextBox to the Text from the first, which works great.
For the Selected Text requirement I started with a similar solution, and my code for this is as follows:
void TextBox1_SelectionChanged(object sender, RoutedEventArgs e)
{
this.TextBox2.Select(this.TextBox1.SelectionStart, this.TextBox1.SelectionLength);
}
This seemed to work pretty well when initially used with a mouse:
But I'm having a problem when selecting text with Touch. I double-tap within the TextBox to create the first "anchor" as you do in Touch, then drag to begin the selection; but I only ever manage to select a single character normally before the selection stops. The TextBox doesn't lose focus exactly, but the behaviour is similar to that; the selection anchors disappear and I can't continue selecting anything unless I re-double-tap to start a new selection. If I remove the code to select text in TextBox2 then the Touch selection behaves perfectly in TextBox1.
I've been trying to fix this for a while and cannot, I'm not sure if I can get the desired behaviour with WinRT TextBoxes. Does anyone have any ideas? Or perhaps another way to implement a solution with two TextBoxes with this behaviour?
Thanks a lot.
So this is far from an answer, but discovered a few things that maybe will help you or others come up with a potential workaround. Apologies if these are things you've already seen and noted.
First, it's not the call to TextBox2.Select() that's the problem per se. This for instance, works fine for me
private void txt1_SelectionChanged(object sender, RoutedEventArgs e)
{
var start = TextBox1.SelectionStart;
var length = TextBox1.SelectionLength;
TextBox2.Select(3, 5);
}
unfortunately, using start and length versus the hard-coded 3 and 5, that is, the following, DOES NOT WORK:
private void txt1_SelectionChanged(object sender, RoutedEventArgs e)
{
var start = TextBox1.SelectionStart;
var length = TextBox1.SelectionLength;
TextBox2.Select(start, length);
}
I also discovered that I could select TWO characters if I started from the end, but only one from the beginning. That got me to thinking about dispatching the call to set the second selection:
private void txt1_SelectionChanged(object sender, RoutedEventArgs e)
{
var start = TextBox1.SelectionStart;
var length = TextBox1.SelectionLength;
Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Low,
() => TextBox2.Select(start, length));
}
Now I can select 2 from the front and 3 and sometimes 4 from the back. Took it a step further, and was able to select as many as six or seven with a really fast swipe.
private void txt1_SelectionChanged(object sender, RoutedEventArgs e)
{
var start = TextBox1.SelectionStart;
var length = TextBox1.SelectionLength;
Dispatcher.RunIdleAsync((v) => Highlight());
}
public void Highlight()
{
TextBox2.Select(TextBox1.SelectionStart, TextBox1.SelectionLength);
}
Seems like the trick to working around this is not setting TextBox2 until whatever vestiges of the TextBox1 SelectionChanged event have completed.
This may be worth registering on Connect.
Mine is only a partial solution as well.
I did some debugging and noticed that the SelectionChanged event is fired throughout the text selection process. In other words, a single finger "swipe" will generate multiple SelectionChanged events.
As you found out, calling TextBox.Select during a text selection gesture affects the gesture itself. Windows seems to stop the gesture after the programmatic text selection.
My workaround is to delay as long as possible calling the TextBox.Select method. This does work well, except for one edge case. Where this method fails is in the following scenario:
The user begins a select gesture, say selecting x characters. The user, without taking their finger off the screen, pauses for a second or two. The user then attempts to select more characters.
My solution does not handle the last bit in the above paragraph. The touch selection after the pause does not actually select anything because my code will have called the TextBox.Select method.
Here is the actual code. As I mentioned above, there are multiple selection changed events fired during a single selection gesture. My code uses a timer along with a counter to only do the programmatic selection when there are no longer any pending touch generated selection changed events.
int _selectCounter = 0;
const int SELECT_TIMER_LENGTH = 500;
async private void TextBox1_SelectionChanged(object sender, RoutedEventArgs e)
{
// _selectCounter is the number of selection changed events that have fired.
// If you are really paranoid, you will want to make sure that if
// _selectCounter reaches MAX_INT, that you reset it to zero.
int mySelectCount = ++_selectCounter;
// start the timer and wait for it to finish
await Task.Delay(SELECT_TIMER_LENGTH);
// If equal (mySelectCount == _selectCounter),
// this means that NO select change events have fired
// during the delay call above. We only do the
// programmatic selection when this is the case.
// Feel free to adjust SELECT_TIMER_LENGTH to suit your needs.
if (mySelectCount == _selectCounter)
{
this.TextBox2.Select(this.TextBox1.SelectionStart, this.TextBox1.SelectionLength);
}
}

Event handler firing for all controls instead of individually

I am having a rather odd problem with the Gecko Webbrowser control, I have created my own class which inherits off of the Gecko Webcontrol and within the constructor of this I have set an event:
class fooGeckoClass: Gecko.GeckoWebBrowser
{
public fooGeckoClass()
{
this.DomClick += new EventHandler<Gecko.GeckoDomEventArgs>(fooEventFunction);
}
private static void fooEventFunction(Object sender, Gecko.GeckoDomEventArgs e)
{
((Gecko.GeckoWebBrowser)sender).Navigate("www.foo.com");
}
}
I am using three of these controls in a manually created UserControl, the controls are loaded in dynamically at start up from a config file and added the the UserControl controls collection. When clicking on any of the three controls, all three will navigate to "www.foo.com" away from there original site. I had a look at:
e.StopPropagation();
Which specifies that it stops further propagation of events during an event flow, however it does also specify that it will handle all events in the current flow, I believe the events must have already been given to the controls before this has a chance to stop it as the the three controls will still fire the event. I also tried e.Handled = true to no avail. Has anyone encountered this problem before and have any kind of solution to make it only fire on the control that was clicked on?
EDIT:
It may be worth showing how the controls are added to the form seeing as this must be where the problem is occurring (it does not happen if the controls are just placed in a user control in a small test app).
private void fooUserControl_Load(object sender, EventArgs e)
{
if (!this.DesignMode)
{
for (int iControls = 0; iControls < geckObs.Count(); iControls ++)
{
fooGeckoClass geckControl = new fooGeckoClass();
this.Controls.Add(geckControl );
break;
}
}
}
Odd answer but I seem to have resolved the issue, DomClick was being called at first run, changing to DomMouseClick or DomMouseUp has completely resolved the issue. I assume DomClick must be an event unto itself as it also doesn't use the GeckoDomMouseEventArgs but the regular GeckoEventArgs.
EDIT:
To add to this, the site I was going to was actually calling DomClick when it had finished loading hence the reason it was being called at start up across all three browsers.

Categories