UWP ScrollViewer - Differentiate between user scrolling and programmatic scrolling - c#

I have an application where I need to take a certain action when the user gets to a certain place in a ScrollViewer. This action sometimes includes scrolling the ScrollViewer to a different location programmatically.
In order to moniter the user's scrolling action, I am listening for the ViewChanged event of the ScrollViewer. The issue is that when I scroll progrmatically from within the ViewChanged event handler, that same event handler ends up getting called again, causing undesired additional scrolling to happen.
I have tried creating a custom method to remove the event handler before calling ScrollViewer.ChangeView(), but this seems to have no effect.
Can anyone come up with a way around this issue, or a way to differentiate the user's scrolling action from my programmatic one?
private void MyScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
if (conditionals)
{
ScrollTo(location);
}
}
private void ScrollTo(double offset)
{
MyScrollViewer.ViewChanged -= MyScrollViewer_ViewChanged;
MyScrollViewer.ChangeView(offset, null, null);
MyScrollViewer.ViewChanged += MyScrollViewer_ViewChanged;
}

It is, unfortunately, not possible to determine what triggered a ViewChanged event. It is however possible to solve this problem.
The issue is that ChangeView() is asynchronous, so re-adding the event handler immediately after calling ChangeView is too soon. ChangeView will raise a bunch of ViewChanged events with a final one where e.IsIntermediate == false; only once that happens should you re-hook the event handler. The best way to handle this might be to use a temporary event handler that waits for that e.IsIntermediate == false and then re-hooks the original handler.
To prevent the user from interacting with the ScrollViewer during the execution of ChangeView, the scroll and zoom modes can be temporarily disabled.
Finally, if the user is manipulating the ScrollViewer when the conditionals are met, that manipulation needs to be canceled before calling ScrollTo().
EDIT: In my implementation, an issue arose where because of the number of times these handlers were called, event handlers were added more than once. To combat this, I've taken the simple strategy from here.
private void MyScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
if (!conditionals) return;
if (e.IsIntermediate)
{
var uiElement = MyScrollViewer.Content as UIElement;
uiElement?.CancelDirectManipulations();
}
ScrollTo(location);
}
private void Temporary_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
if (e.IsIntermediate) return;
MyScrollViewer.ViewChanged -= Temporary_ViewChanged;
MyScrollViewer.ViewChanged -= MyScrollViewer_ViewChanged;
MyScrollViewer.ViewChanged += MyScrollViewer_ViewChanged;
MyScrollViewer.HorizontalScrollMode = ScrollMode.Enabled;
MyScrollViewer.VerticalScrollMode = ScrollMode.Enabled;
MyScrollViewer.ZoomMode = ZoomMode.Enabled;
}
private void ScrollTo(double offset)
{
MyScrollViewer.ViewChanged -= MyScrollViewer_ViewChanged;
MyScrollViewer.ViewChanged -= Temporary_ViewChanged;
MyScrollViewer.ViewChanged += Temporary_ViewChanged;
MyScrollViewer.HorizontalScrollMode = ScrollMode.Disabled;
MyScrollViewer.VerticalScrollMode = ScrollMode.Disabled;
MyScrollViewer.ZoomMode = ZoomMode.Disabled;
MyScrollViewer.ChangeView(offset, null, null);
}

Related

Is there anyway to prevent some event fired before end of another event?

In the code below the SelectionChanged event is fired before the end of RowsAdded,how can I make the Event atomic?
private void dataGridView1_RowsAdded(object sender, DataGridViewRowsAddedEventArgs e)
{
dataGridView1.CurrentCell = dataGridView1.Rows[dataGridView1.Rows.Count - 1].Cells[1];
dataGridView1.Rows[dataGridView1.Rows.Count - 1].Cells[1].Selected = true;
}
private void dataGridView1_SelectionChanged(object sender, EventArgs e)
{
if (dataGridView1.CurrentRow != null)
{
//Something
}
}
what should i do?
SelectionChanged is fired in the middle of handling RowsAdded because you are causing a SelectionChanged by changing the current cell within dataGridView1_RowsAdded. Adding a row doesn't cause both events to be fired -- you're causing the second event while handling the first one. (In fact, you're probably causing SelectionChanged twice, because both lines in the handler seem to change the selection).
If you don't want dataGridView1_SelectionChanged running while in the RowsAdded handler, you need to either temporarily unsubscribe from the event:
dataGridView1.SelectionChanged -= dataGridView1_SelectionChanged;
// change the selection...
dataGridView1.SelectionChanged += dataGridView1_SelectionChanged;
Or even better, re-design what you're doing inside the SelectionChanged handler so that it is appropriate for all instances of the event.
You can override the event you want to conrol and then put an if condition in the overriden event and control when it fires and when it does not.
mahdi

trouble creating custom event handler

I've been trying to get my head around creating a custom event to handle mouse controls and such, but i keep running into certain errors that none of the tutorials seem to address. I'm pretty sure my logic of creation / assigning is correct, but maybe theres something fundamentally wrong that I'm missing here.
First off i create my delegate with the same signature as the method;
public delegate void MouseDown(object sender, MouseEventArgs e, Control control);
And then i assign it my event;
public event MouseDown OnMouseDown;
and then finally i try and subscribe to the event;
public static void Init(Control control, Control container, Direction direction)
{
control.MouseDown += OnMouseDown;
}
//ignore the parameters I'm passing, these are out of the scope of my problem.
However on this section I'm getting the error "an ohject reference is required for the non-static field, method, or propery "blah blah.OnMouseDown""
Finally heres my method I'm trying to subscribe to on mouseDown;
public void MouseDownEvent(object sender, MouseEventArgs e, Control control)
{
Dragging = true;
DragStart = new Point(e.X, e.Y);
control.Capture = true;
}
It probably doesn't help that I'm trying to modify a helper class i found somewhere. If any further information is needed feel free to ask.
Note: The prime objective of this class is to allow controls to be moved at runtime.
Edit:
I believe the first two have worked, but to move i need to use the following method;
public void MouseMoveEvent(object sender, MouseEventArgs e, Control control, Control container, Direction direction)
{
if (Dragging)
{
if (direction != Direction.Vertical)
container.Left = Math.Max(0, e.X + container.Left - DragStart.X);
if (direction != Direction.Horizontal)
container.Top = Math.Max(0, e.Y + container.Top - DragStart.Y);
}
}
so i need to send Direction direction, which i can't send as sender. The reason I'm making these changes to the whole system is, i had it working before using anonymous delegates, but these proved tough to unsubscribe from when i wanted to re-lock a control in place.
Edit 2:
scratch that, the mouseDown and mouseUp won't work if i don't pass the correct control, at least subscribing the way i was doing it before. i could try your method but, the way i was doing it i was just calling one method which subscribed to all 3 MouseControls. it looks like either i can subscribe in sepearate methods as suggest, or i need to pass the right control correctly, i.e. not sender as control. any ideas?
Currently I'm subscribing by running this method from anywhere;
helper.Init(this.Controls["btn" + i]);
and then it runs through these methods before subscribing the button to my mouseup, down and move.
public static void Init(Control control)
{
Init(control, Direction.Any);
}
public static void Init(Control control, Direction direction)
{
Init(control, control, direction);
}
public static void Init(Control control, Control container, Direction direction)
{
control.MouseDown += new MouseEventHandler(FireOnMouseDown);
control.MouseUp += new MouseEventHandler(FireOnMouseUp);
control.MouseMove += delegate(object sender, MouseEventArgs e)
{
if (Dragging)
{
if (direction != Direction.Vertical)
container.Left = Math.Max(0, e.X + container.Left - DragStart.X);
if (direction != Direction.Horizontal)
container.Top = Math.Max(0, e.Y + container.Top - DragStart.Y);
}
};
}
Note: the third subscription is how they were before (anon delegates). I believe i need to pass the correct control in the events though. Does this give more clarity to my problem?
Answer to this question:
However on this section i'm getting the error "an ohject reference is
required for the non-static field, method, or propery "blah
blah.OnMouseDown""
Init method is static which means that any non local variable used inside it must be static. In your code, the public event MouseDown OnMouseDown; must be static.
Just do this and it will work fine (without the need for a delegate):
EDIT
Please see code below to see how to get the control that has been clicked.
public static void Init(Control control, Control container, Direction direction)
{
control.MouseDown += new System.Windows.Forms.MouseEventHandler(On_MouseDown);;
}
private static void On_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{
Control control = sender as Control;
if(control != null){
// Here we go, use the control to do whatever you want with it
}
}
You should write:
control.MouseDown += new MouseEventHandler(MouseDownEvent);
Be careful here that you must change the signature of MouseDownEvent, here, since the MouseDown event of Control needs only the sender and the MouseEventArgs as parameters.
But I'm not sure if this is what you really want. The effect of this instruction is that when the mouse goes down on the control MouseDownEvent is executed. Is this what you want?
The instruction public event MouseDown OnMouseDown; is unesuful unless somewhere in the same class where this appears you do not write something that fires this event in this way:
if (OnMouseDown!= null)
OnMouseDown(aSenderObject, aMouseEventArgs, aControl);
And to subscribe to thgis event you should write this (here hc anObj is an instance of the class where the event is defined):
anObj.OnMouseDown+= new MouseDown(MouseDownEvent);
Hope it helps. Maybe a little more context could be useful.
EDIT:
If you are trying to implement a class that manages the movement of controls you could have the Helper class that contains:
public delegate void MouseDown(object sender, MouseEventArgs e, Control control);
public static event MouseDown OnMouseDown;
If you want to be able to "register" a control to your class this is the way to do it (note that the event is static). Here you say that when the mouse goes down on control, FireMouseDown is executed.
public static void Init(Control control, Control container, Direction direction)
{
control.MouseDown += new MouseEventHandler(FireOnMouseDown);
}
In FireMouseDown you have to fire the OnMouseEvent with the right arguments:
public static void FireOnMouseDown(object sender, MouseEventArgs e)
{
if (OnMouseDown != null)
OnMouseDown(this, e, sender as Control);
}
Now you can subscribe to OnMouseDown your original method from outside Helper:
Helper.OnMouseDown += new MouseDown(MouseDownEvent);
EDIT2:
I didn't specify it, but what's happening here is that the OnMouseDown event is fired everytime there is a MouseDown event (the windows event, I mean) on one of the controls that were passed to Init. This means that the registration to OnMouseDown has to be done only once. After that, every time you have a MouseDown on one of these controls MouseDownEvent is executed passing these parameters:
sender -> the Helper class
e -> the original MouseEventArgs generated by Windows
control -> the control that generated the original event
With regard to the MouseMove event, you should do something like for MouseDown, just adding the extra parameters you need (even if it's not clear to me what is the meaning of container and direction, since it seems that container is always equal to control and direction is always Direction.Any). I mean something like:
public delegate void MouseMove(object sender, MouseEventArgs e, Control control, Control container, Direction direction);
public static event MouseMove OnMouseMove;
public static void FireOnMouseMove(object sender, MouseEventArgs e)
{
if (OnMouseMove != null)
OnMouseMove(this, e, sender as Control, aContainer, aDirection);
}
What I don't understand here is from where you are going to find out aContainer and aDirection. If I should replicate your Init I would write OnMouseMove(this, e, sender as Control, sender as Control, Direction.Any);, but I don't think that it would work.

Track Bar Only fire event on final value not ever time value changes

I am working on a pretty basic C# visual studio forms application but am having some issue getting the track bar to act as I want it to so hoping someone in the community might have a solution for this.
What I have is a pretty basic application with the main part being a track bar with a value of 0 to 100. The user sets the value of the track to represent "the amount of work to perform" at which point the program reaches out to some devices and tells them to do "x" amount of work (x being the value of the trackbar). So what I do is use the track bars scroll event to catch when the track bars value has changed and inside the handler call out to the devices and tells them how much work to do.
My issue is that my event handler is called for each value between where the track bar currently resides and where ever it ends. So if it is slid from 10 to 30, my event handler is called 20 times which means I am reaching out to my devices and telling them to run at values I don't even want them to run at. Is there someway only to event when scroll has stopped happening so you can check the final value?
Just check a variable, if the user clicked the track bar. If so, delay the output.
bool clicked = false;
trackBar1.Scroll += (s,
e) =>
{
if (clicked)
return;
Console.WriteLine(trackBar1.Value);
};
trackBar1.MouseDown += (s,
e) =>
{
clicked = true;
};
trackBar1.MouseUp += (s,
e) =>
{
if (!clicked)
return;
clicked = false;
Console.WriteLine(trackBar1.Value);
};
For the problem #roken mentioned, you can set LargeChange and SmallChange to 0.
Try the MouseCaptureChanged event - that is the best for this task
A user could also move the track bar multiple times in a short period of time, or click on the track multiple times to increment the thumb over instead of dragging the thumb. All being additional cases where the value that registers at the end of a "thumb move" is not really the final value your user desires.
Sounds like you need a button to confirm the change, which would then capture the current value of the trackbar and send it off to your devices.
Try this with the trackbar_valuechanged event handler:
trackbar_valuechanged(s,e) {
if(trackbar.value == 10){
//Do whatever you want
} else{
//Do nothing or something else
}
}
I found a fairly reliable way to do this is to use a timer hooked up in the trackbar.Scroll event:
private Timer _scrollingTimer = null;
private void trackbar_Scroll(object sender, EventArgs e)
{
if (_scrollingTimer == null)
{
// Will tick every 500ms (change as required)
_scrollingTimer = new Timer()
{
Enabled = false,
Interval = 500,
Tag = (sender as TrackBar).Value
};
_scrollingTimer.Tick += (s, ea) =>
{
// check to see if the value has changed since we last ticked
if (trackBar.Value == (int)_scrollingTimer.Tag)
{
// scrolling has stopped so we are good to go ahead and do stuff
_scrollingTimer.Stop();
// Do Stuff Here . . .
_scrollingTimer.Dispose();
_scrollingTimer = null;
}
else
{
// record the last value seen
_scrollingTimer.Tag = trackBar.Value;
}
};
_scrollingTimer.Start();
}
}
I had this problem just now as I'm implementing a built in video player and would like the user to be able to change the position of the video but I didn't want to overload the video playback API by sending it SetPosition calls for every tick the user passed on the way to his/her final destination.
This is my solution:
First, the arrow keys are a problem. You can try your best to handle the arrow keys via a timer or some other mechanism but I found it more pain than it is worth. So set the property SmallChange and LargeChange to 0 as #Matthias mentioned.
For mouse input, the user is going to have to click down, move it, and let go so handle the MouseDown, MouseUp, and the Scroll events of the trackbar like so:
private bool trackbarMouseDown = false;
private bool trackbarScrolling = false;
private void trackbarCurrentPosition_Scroll(object sender, EventArgs e)
{
trackbarScrolling = true;
}
private void trackbarCurrentPosition_MouseUp(object sender, MouseEventArgs e)
{
if (trackbarMouseDown == true && trackbarScrolling == true)
Playback.SetPosition(trackbarCurrentPosition.Value);
trackbarMouseDown = false;
trackbarScrolling = false;
}
private void trackbarCurrentPosition_MouseDown(object sender, MouseEventArgs e)
{
trackbarMouseDown = true;
}
I had a similar problem, only with a range TrackBar Control. Same idea applies to this also, only it's easier for this case.
I handled the MouseUp Event on the TrackBar to launch the procedures I needed, only after you would let go of the mouse button. This works if you drag the bar to your desired position or just click it.
private void rangeTrackBarControl1_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
{
YourProcedureHere();
}
i solved the problem for my application with two events:
catch the Trackbar-ValueChange-Event
whithin the value-change event disable the valuechange event and enable the MouseUp-Event
public MainWindow()
{
//Event for new Trackbar-Value
trackbar.ValueChanged += new System.EventHandler(trackbar_ValueChanged);
}
private void trackbar_ValueChanged(object sender, EventArgs e)
{
//enable Trackbar Mouse-ButtonUp-Event
trackbar.MouseUp += ch1_slider_MouseUp;
//disable Trackbar-ValueChange-Event
trackbar.ValueChanged -= ch1_slider_ValueChanged;
}
private void trackbar_MouseUp(object sender, EventArgs e)
{
//enable Trackbar-ValueChange-Event again
trackbar.ValueChanged += new System.EventHandler(trackbar_ValueChanged);
//disable Mouse-ButtonUp-Event
trackbar.MouseUp -= trackbar_MouseUp;
//This is the final trackbar-value
textBox.AppendText(trackbar.Value);
}
ATTENTION: this works if the trackbar is moved by mose. It is also possible to move the trackbar by keyboard. Then futher code must be implemented to handle this event.

Prevent circular event chaining on UI controls when updated programmatically

I have a CheckedListBox which contains many items which can be checked or unchecked individually.
Next to this are CheckBoxes representing supersets of the many items opposite. These are tri-state corresponding to what portion of their items are selected opposite.
Both the CheckedListBox and individual CheckBoxes subscribe to check changed type events which is causing a circular reference and overflow. How can I disable one from receiving change events when the other is updating it, programmatically?
void checkBox1_N_CheckStateChanged(object sender, EventArgs e) {
checkedListBox1.ItemCheck -= this.CheckedListBox1_ItemCheck;
// Handler body where affecting CheckedListBox1.
checkedListBox1.ItemCheck += this.CheckedListBox1_ItemCheck;
}
It works best to disable each others' event handlers as the other is updating.
void CheckedListBox1_ItemCheck(object sender, EventArgs e) {
//Find which 'i' is affected.
arrOfCBox[i].cb.CheckStateChanged -= arrOfCBox[i].eh;
// Handler body where affecting checkBox[i].
arrOfCBox[i].cb.CheckStateChanged += arrOfCBox[i].eh;
}
where BoxHandler is, at a minimum:
class BoxHandler
{
public EventHandler eh;
public CheckBox cb;
}
Similar to WinForms: temporarily disable an event handler, but amazingly that didn't get awarded an answer, (prolly too abstract?).

Is there a "All Children loaded" event in WPF

I am listening for the loaded event of a Page. That event fires first and then all the children fire their load event. I need an event that fires when ALL the children have loaded. Does that exist?
I hear you. I also am missing an out of the box solution in WPF for this.
Sometimes you want some code to be executed after all the child controls are loaded.
Put this in the constructor of the parent control
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() => {code that should be executed after all children are loaded} ));
Helped me a few times till now.
Loaded is the event that fires after all children have been Initialized. There is no AfterLoad event as far as I know. If you can, move the children's logic to the Initialized event, and then Loaded will occur after they have all been initialized.
See MSDN - Object Lifetime Events.
You can also use the event: ContentRendered.
http://msdn.microsoft.com/en-us/library/ms748948.aspx#Window_Lifetime_Events
WPF cant provide that kind of an event since most of the time Data is determining whther to load a particular child to the VisualTree or not (for example UI elements inside a DataTemplate)
So if you can explain your scenario little more clearly we can find a solution specific to that.
One of the options (when content rendered):
this.LayoutUpdated += OnLayoutUpdated;
private void OnLayoutUpdated(object sender, EventArgs e)
{
if (!isInitialized && this.ActualWidth != 0 && this.ActualHeight != 0)
{
isInitialized = true;
// Logic here
}
};
Put inside your xaml component you want to wait for, a load event Loaded="MyControl_Loaded" like
<Grid Name="Main" Loaded="Grid_Loaded"...>
<TabControl Loaded="TabControl_Loaded"...>
<MyControl Loaded="MyControl_Loaded"...>
...
and in your code
bool isLoaded;
private void MyControl_Loaded(object sender, RoutedEventArgs e)
{
isLoaded = true;
}
Then, inside the Event triggers that have to do something but were triggering before having all components properly loaded, put if(!isLoaded) return; like
private void OnButtonChanged(object sender, RoutedEventArgs e)
{
if(!isLoaded) return;
... // code that must execute on trigger BUT after load
}
I ended up doing something along these lines.. your milage may vary.
void WaitForTheKids(Action OnLoaded)
{
// After your children have been added just wait for the Loaded
// event to fire for all of them, then call the OnLoaded delegate
foreach (ContentControl child in Canvas.Children)
{
child.Tag = OnLoaded; // Called after children have loaded
child.Loaded += new RoutedEventHandler(child_Loaded);
}
}
internal void child_Loaded(object sender, RoutedEventArgs e)
{
var cc = sender as ContentControl;
cc.Loaded -= new RoutedEventHandler(child_Loaded);
foreach (ContentControl ctl in Canvas.Children)
{
if (!ctl.IsLoaded)
{
return;
}
}
((Action)cc.Tag)();
}

Categories