Debugging a Custom Windows.Forms.ComboBox - winforms

Problem - whenever I set a custom ComboBox to "ON," it will change to "ON" momentarily, but it will then occasionally switch to "OFF."
Basically I have a method:
void value_SelectedIndexChanged(object sender, EventArgs e)
{
if((ComboBox)sender.Focused == true)
{
if(value.SelectedIndex == false) // OFF
// set a bunch of variables for OFF
else // ON
// set a bunch of variables for ON
}
}
After I set the ComboBox to "ON," it then executes the code path for "ON" selected index. But then, the selected index switches to "OFF," for a reason I do not understand. As a result, the OFF variables code path then executes.
To attempt to fix it, I put a log statement to capture the sender and EventArgs e whenever this "value_SelectedIndexChanged" method gets called. However the sender equals the class and the EventArgs is System.EventArgs.
Please advise me on how to debug this problem.

I don't quite understand why you structure your code that way. Wouldn't it make more sense this way:
void value_SelectedIndexChanged(object sender, EventArgs e)
{
switch(value.SelectedIndex)
{
case 0: // OFF
// set a bunch of variables for OFF
break;
case 1: // ON
// set a bunch of variables for ON
break;
}
}
The problem might be caused by the "set a bunch of variables for OFF"-actions. Have you tested that by setting a breakpoint in the handler and then stepping through everything?

Related

Scrolling in Sticky Notes

I got the following sticky note example:
If the sticky note has more than 9 rows, the additional rows are not visible.
I'm able to navigate through the note with my arrow keys. If I'm going to scroll with the mouse wheel, it seems to ignore the popup and just changes the page.
Is it possible to activate scrolling for sticky note popups?
Edit:The solution outlined below will soon be available as part of the samples included in the PDFTron SDK download. In the meanwhile, I hope that the below solution helps.
Yes, it is possible to activate scrolling for sticky notes.
The problem is most apparent when using the single page view. It appears to work as expected in continuous mode.
However it is not as simple as setting VerticalScrollVisibility = ScrollBarVisibility.Auto;. There are a few files that need to be modified to get this working.
The good news is that we can get the expected behaviour by modifying the code in the provided samples.
Solution
The solution is to add some handling for the PreviewMouseWheel event coming from the PDFViewWPF class.
In the downloaded samples, the following changes were made to get things running as expected:
Add a method to handle the PreviewMouseWheel event in the NoteHost class (Samples/PDFViewWPFTools/CS/Utilities/NoteHost.cs)
internal void HandlePreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
var originalSource = (UIElement)e.OriginalSource;
if (originalSource.IsDescendantOf(mNoteBorder) && mTextBox.IsFocused)
{
mTextBox.ScrollToVerticalOffset(mTextBox.VerticalOffset - e.Delta);
e.Handled = true;
}
}
Also make sure to add mTextBox.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; in the NoteHost.CreateNoteAndArrow() method, after the mTextBox object is instantiated (~line 183).
Next, edit the NoteManager class - Samples/PDFViewWPFTools/CS/Utilities/NoteManager.cs - and add a HandlePreviewMouseWheel method. This will internally call the HandlePreviewMouseWheel on each displayed (opened) note and break at the first one where the event gets handled.
internal void HandlePreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
{
foreach(var note in mActiveNotes)
{
note.Value.HandlePreviewMouseWheel(sender, e);
if(e.Handled)
{
break;
}
}
}
Next, edit the ToolManager class to ensure that the note manager gets a chance to handle the PreviewMouseWheel before attempting a page change. Open Samples/PDFViewWPFTools/CS/ToolManager.cs and navigate to the PDFView_PreviewMouseWheel. The existing method should look like this:
private void PDFView_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
{
if (mCurrentTool != null && _IsEnabled)
{
ToolManager.ToolType prev_tm = mCurrentTool.ToolMode;
ToolManager.ToolType next_tm;
while (true)
{
mCurrentTool.PreviewMouseWheelHandler(sender, e);
next_tm = mCurrentTool.NextToolMode;
if (prev_tm != next_tm)
{
mCurrentTool = CreateTool(next_tm, mCurrentTool);
prev_tm = next_tm;
}
else
{
break;
}
}
}
}
Replace it with the below code:
private void PDFView_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
{
if (mCurrentTool != null && _IsEnabled)
{
ToolManager.ToolType prev_tm = mCurrentTool.ToolMode;
ToolManager.ToolType next_tm;
while (true)
{
mNoteManager.HandlePreviewMouseWheel(sender, e);
if (!e.Handled)
{
mCurrentTool.PreviewMouseWheelHandler(sender, e);
next_tm = mCurrentTool.NextToolMode;
if (prev_tm != next_tm)
{
mCurrentTool = CreateTool(next_tm, mCurrentTool);
prev_tm = next_tm;
}
else
{
break;
}
}
else
{
break;
}
}
}
}
By doing the above, we are giving the NoteManager a chance to handle the PreviewMouseWheel before doing anything else with it.
Another point to note is that we have to now "do the scrolling" in code, using the mTextBox.ScrollToVerticalOffset method in the NoteHost class.

Windows Forms: background worker synchronization and management

I have a problem with following, very simplified case being part of my project. Consider we have GUI like below:
I have two background workers:
plot_bgworker - in this example, it increments plot counter,
data_bgworker - in this example, it increments data counter.
I also have label_timer, which updates incremented values diplayed on my form.
To manage both background workers and timer, I wrote two functions:
private: void turnOnAcquisition() {
if (!counting_paused)
return;
if (!plot_bgworker->IsBusy)
plot_bgworker->RunWorkerAsync();
if (!data_bgworker->IsBusy)
data_bgworker->RunWorkerAsync();
label_timer->Enabled = true;
counting_paused = false;
}
private: void turnOffAcquisition() {
if (counting_paused)
return;
if (plot_bgworker->IsBusy)
plot_bgworker->CancelAsync();
if (data_bgworker->IsBusy)
data_bgworker->CancelAsync();
label_timer->Enabled = false;
counting_paused = true;
}
Then, here is what happens when I click each of my buttons:
// Pauses counting on click
private: System::Void stop_btn_Click(System::Object^ sender, System::EventArgs^ e) {
turnOffAcquisition();
}
// Starts counting on click
private: System::Void start_btn_Click(System::Object^ sender, System::EventArgs^ e) {
turnOnAcquisition();
}
// Should restart counting on click, beginning from 0 (no matter what state counting is in right now)
private: System::Void restart_btn_Click(System::Object^ sender, System::EventArgs^ e) {
plot_counter = 0;
data_counter = 0;
turnOffAcquisition();
turnOnAcquisition();
}
Finally, here are my background workers (turned off / on by CancelAsync() / RunWorkerAsync() ) and timer:
// Calculating data counter
private: System::Void data_bgworker_DoWork(System::Object^ sender, System::ComponentModel::DoWorkEventArgs^ e) {
for (;;) {
data_counter++;
Sleep(50);
if (data_bgworker->CancellationPending) {
e->Cancel = true;
return;
}
}
}
// Calculating plot counter
private: System::Void plot_bgworker_DoWork(System::Object^ sender, System::ComponentModel::DoWorkEventArgs^ e) {
for (;;) {
plot_counter++;
Sleep(120);
if (plot_bgworker->CancellationPending) {
e->Cancel = true;
return;
}
}
}
// Display counters
private: System::Void label_timer_Tick(System::Object^ sender, System::EventArgs^ e) {
plot_counter_label->Text = numToMStr(plot_counter);
data_counter_label->Text = numToMStr(data_counter);
}
Start button and stop button work both as expected, but now I have a problem with restart button. When I click it in the middle of counting, it seems to reset values and stop background workers, but never start them again (as I would expect after calling turnOnAcquisition). However, when I click it when counting is off, I am able to turn on counting as expected.
My first shot was that cancellation flag is not yet set to another value when I tried to check if my workers were busy, but using Sleep() between calls didn't work. Another guess is that it is due to race condition failure, so I tried using MemoryBarrier(), but I don't know the libraries and I'm not sure if it would work. Also, I tried to use Interlocked class, but couldn't use it properly for void functions.
1. Is this way of thinking correct?
2. If yes, why simple Sleep() doesn't do the trick?
3. How would I use any of mentioned methods in this case and which one would be the best match?
Ok, I found the solution by myself. The problem here was about the race condition - one event tried to stop counting (which meant raising another event) and then starting it again (which was problematic, as my function (I guess) was already cluttered with the first one and probably the second event wasn't even added to the event detected queue). If I am wrong with the explanation, I would appreciate some criticism down there ;)
Here are two modified functions, which solved thread management correctly. The key was to let the other events do their work until I get desired state.
When I want to turn off counting, I let the applications do the events from the queue until both threads will not be busy (the 'while' loop):
private: void turnOffAcquisition() {
if (counting_paused)
return;
if (plot_bgworker->IsBusy)
plot_bgworker->CancelAsync();
if (data_bgworker->IsBusy)
data_bgworker->CancelAsync();
while((plot_bgworker->IsBusy) || (data_bgworker->IsBusy)) // Continue to process events until both workers stop working
Application::DoEvents(); // Then, you can process another thread requests! :)
label_timer->Enabled = false;
counting_paused = true;
}
Similarily, when I want to restart counting, I let the application do the events until I check that both threads are busy (again, the 'while' loop):
private: void turnOnAcquisition() {
if (!counting_paused)
return;
if (!plot_bgworker->IsBusy)
plot_bgworker->RunWorkerAsync();
if (!data_bgworker->IsBusy)
data_bgworker->RunWorkerAsync();
while((!plot_bgworker->IsBusy) || (!data_bgworker->IsBusy)) // Continue to process events until both workers start working
Application::DoEvents(); // Then, you can process another thread requests! :)
label_timer->Enabled = true;
counting_paused = false;
}

Working with ProgressBar and ComboBox

I'm in trouble with a Marquee ProgressBar. I need to execute a method (refreshList()) to get a List<string>. Then I assign this List to a ComboBox, so ComboBox refreshes with the new Items. As refreshList() take 3 or 4 sec, I wanted to run a Marquee ProgressBar. But I couldn't. ProgressBar is ok, but ComboBox doesn't load new Items.
My refreshList() method:
private void refreshList(List<string> list)
{
albumList.DataSource = null;
albumList.DataSource = list;
}
I have the following code, it works fine:
private void changeDirectoryToolStripMenuItem_Click(object sender, EventArgs e)
{
fbd.RootFolder = Environment.SpecialFolder.MyComputer;
folderPath = "";
if (fbd.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
folderPath = fbd.SelectedPath;
refreshList(N.getList(folderPath));
}
}
But I added a ProgressBar and wrote this code:
private void changeDirectoryToolStripMenuItem_Click(object sender, EventArgs e)
{
fbd.RootFolder = Environment.SpecialFolder.MyComputer;
folderPath = "";
if (fbd.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
folderPath = fbd.SelectedPath;
bgWorker.WorkerReportsProgress = true;
bgWorker.RunWorkerAsync();
}
}
And I placed refreshList() in doWork() method:
private void bgWorker_DoWork(object sender, DoWorkEventArgs e)
{
refreshList(N.getList(folderPath));
}
But unfortunately this isn't working. Can anybody help me solving this problem? Thanks in advance.
You can use the MarqueeAnimationSpeed and Value properties of the ProgressBar control to stop and start the Marquee. There's no need to use WorkerReportsProgress* as you aren't incrementing a normal progress bar - you just want to "spin" the Marquee.
You can do something like the following:
public Form1()
{
InitializeComponent();
//Stop the progress bar to begin with
progressBar1.MarqueeAnimationSpeed = 0;
//If you wire up the event handler in the Designer, then you don't need
//the following line of code (the designer adds it to InitializeComponent)
//backgroundWorker1.RunWorkerCompleted += backgroundWorker1_RunWorkerCompleted;
}
private void changeDirectoryToolStripMenuItem_Click(object sender, EventArgs e)
{
fbd.RootFolder = Environment.SpecialFolder.MyComputer;
folderPath = "";
if (fbd.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
folderPath = fbd.SelectedPath;
//This line effectively starts the progress bar
progressBar1.MarqueeAnimationSpeed = 10;
bgWorker.RunWorkerAsync(); //Calls the DoWork event
}
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
e.Result = N.getList(folderPath); //Technically this is the only work you need to do in the background
}
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
//these two lines effectively stop the progress bar
progressBar1.Value = 0;
progressBar1.MarqueeAnimationSpeed = 0;
//Now update the list with the result from the work done on the background thread
RefreshList(e.Result as List<String>);
}
private void RefreshList(List<String> results)
{
albumList.DataSource = null; //You don't need this line but there is no real harm.
albumList.DataSource = list;
}
Remember to wire up the RunWorkerCompleted event to backgroundWorker1_RunWorkerCompleted via the Properties bar, Events section in the designer.
To begin with, we start the ProgressBar's animation by setting the MarqueeAnimationSpeed property to a non-zero positive number as part of your successful folder selection.
Then, after calling RunWorkerAsync, the code builds your list in the DoWork method, then assigns the result to the DoWorkEventArgs, which get passed to the RunWorkerCompleted event (which fires when DoWork is finished).
In the backgroundWorker1_RunWorkerCompleted method, we stop the progress bar (and set it's value to zero to effectively return it to it's original state), and then we pass the list to the refreshList method to databind it and populate the ComboBox.
Tested using VS2012, Windows Forms, .Net 4.0 (with a Thread.Sleep to emulate the time taken for N.getList)
*WorkerReportsProgress, and the associated ReportProgress method/event are used when you want to increment the progress bar - you can tell the GUI that you are 10% done, 20% done, 50% done etc etc.

WPF Memory Leak from Drag-Drop

Using Red-Gate tools we have detected that the System.Windows.DataObject is holding a reference to a dragObject (a framework element) that is hanging around from an operation long since completed.
How does one "clear" the drag object once DragDrop.DoDragDrop? Is there a way to pass a null through this and have it fall right through?
I just discovered this gem myself, my solution was to use a WeakReference to the data item being dragged.
DataObject data = new DataObject(new WeakReference(this.draggedData));
DragDrop.DoDragDrop((DependencyObject)sender, data, DragDropEffects.Move);
and then in the drop
var draggedItem = e.Data.GetData(this.format.Name) as WeakReference;
if (draggedItem != null && draggedItem.IsAlive)
{
....
}
First of all a big thanks to Ian Oakes for his solution. I needed a slight variant however: I had to make sure that dropping always works, even if the garbage collector ran in the meanwhile. Here is the solution:
public partial class DragDropDemo : Window
{
private SomeDragDropData _dragDropData;
private void OnMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
_dragDropData = new SomeDragDropData { Text = "Some drag data" };
var dataObject = new DataObject("SomeObjectTypeId", new WeakReference<SomeDragDropData>(_dragDropData));
DragDrop.DoDragDrop((DependencyObject)sender, dataObject, DragDropEffects.Move);
_dragDropData = null;
}
}
private void OnDrop(object sender, DragEventArgs e)
{
var weakReferenceData = e.Data.GetData("SomeObjectTypeId") as WeakReference<SomeDragDropData>;
if (weakReferenceData != null && weakReferenceData.IsAlive)
MessageBox.Show(weakReferenceData.Target.Text);
}
}
public class SomeDragDropData
{
public string Text;
}
Some remarks:
The reason this works is because DoDragDrop blocks until the user triggered the drop operation. Hence, _dragDropData is made null only after the drag-drop operation is fully finished.
It is very important to make _dragDropData a member variable. Merely making it a local variable is not enough: when the garbage collector is triggered the object might get disposed. This results in a very hard to reproduce bug because it is not because the garbage collector is triggered that the object necessarily gets cleaned up. From what I saw it only gets cleaned up when a lot of memory got allocated and deallocated

Winforms StatusStrip - why are there periods where it is blank when I'm updating it?

BACKGROUND: I have a WindowForms v3.5 application with a StatusStrip set to be used as a TooStripStatusLabel. I'm issues quite a lot of updates to it during a task that is running, however there are noticable periods where it is BLANK. There are no points when I am writing a blank to the status strip label either.
QUESTION: Any ideas why I would be seeing period where the status strip label is blank, when I don't expect it to be?
How I update it:
private void UpdateStatusStrip(string text)
{
toolStripStatusLabel1.Text = text;
toolStripStatusLabel1.Invalidate();
this.Update();
}
PS. Calling Application.DoEvents() after the this.Update() does not seem to help. I actually am calling this via the backgroundworker control, so:
(a) I start up the background worker:
private void Sync_Button_Click(object sender, EventArgs e)
{
backgroundWorker1.RunWorkerAsync();
DisableUpdateButtons();
}
(b) the background worker calls updates:
private void backgroundWorker1_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
backgroundWorker1.ReportProgress(1, "Example string");
MainForm.MyC.SyncFiles(sender);
}
(c) The MyC business class uses it too, e.g.
public void SyncFiles(object sender)
{
BackgroundWorker bgw = (System.ComponentModel.BackgroundWorker) sender;
bgw.ReportProgress(1, "Starting sync...");
.
.
.
}
(d) This event picks it up:
private void backgroundWorker1_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e)
{
UpdateStatusStrip((string)e.UserState);
}
(e) And again the update status strip
private void UpdateStatusStrip(string text)
{
toolStripStatusLabel1.Text = text;
toolStripStatusLabel1.Invalidate();
this.Update();
}
Does this help?
The reason is possibly in the caller of this function. If you call it from another thread, use Control.BeginInvoke instead of direct call. If you call it from the main application thread during long processing, try Application.DoEvents after UpdateStatusStrip call.

Resources