UI thread slow to respond to Progress updaters on async Task method using VS2022 & Net6.0 - multithreading

I’ve run into a performance obstacle and I’m uncertain of the cause, all of this is running under VS2022 & Net6.0. As this is my 1st time using this combination of a modal windows form, and progress bar, with the work running on a background thread and two Progress objects updating the UI, the progress bar, and a text label, I don’t know where to attack the problem. Prior to placing the workload on a background thread, everything was snappy, searching a thousand files with about 600 lines of text in each, in about a minute. Naturally, the windows form was frozen during this, which is why the workload was placed on a background thread.
After doing so, the workload will be 25-50% complete before the UI starts displaying the values from the Progress objects, and overall, the entire process now takes 10x as long to complete. Progress objects aren’t skipping over any values sent to them, the UI thread just seems slow in getting the information. Likewise, if I try to drag the modal form to a new spot on the desktop it’s unresponsive for 20—30 seconds before it finally moves. One more thing, I can step through the code on the background thread and see it calling the Progress updaters, but the UI thread is just very slow in responding to them.
I could use some suggestions on how to uncover the problem or if clearly evident, point out where the likely problem could be. Here are the essential controls and methods used.
public class SearchProgressForm : Form
{
private System.Windows.Forms.Button btnSearch = new Button();
private System.Windows.Forms.TextBox txtTextSearch = new TextBox();
private System.Windows.Forms.Label lblSearchFile = new Label();
private System.Windows.Forms.ProgressBar SearchProgressBar = new ProgressBar();
public event LogSearchEventHandler SearchSucceededEvent;
protected void OnSearchSucceeded(LogSearchEventArguments p_eventArguments)
{
LogSearchEventHandler handler = SearchSucceededEvent;
if (handler != null)
{
handler(this, p_eventArguments);
}
}
private void InitializeComponent()
{
this.btnSearch.Name = "btnSearch";
this.btnSearch.Text = "Search";
this.btnSearch.Click += new System.EventHandler(this.btnSearch_Click);
this.lblSearchFile.Text = "Searching File: ";
this.txtTextSearch.Text = "search string";
}
public SearchProgressForm() { }
private void btnSearch_Click(object sender, EventArgs e)
{
this.SearchByText(this.txtTextSearch.Text);
}
private void SearchByText(string p_searchParameter)
{
// Setup a progress report for thr ProgressBar
var _progressBarUpdate = new Progress<int>(value =>
{
this.SearchProgressBar.Value = value;
this.SearchProgressBar.Refresh();
});
var _progressFileNameUpdate = new Progress<string>(value =>
{
this.lblSearchFile.Text = "Searching File For : " + value;
this.lblSearchFile.Refresh();
});
// Start search on a backgroud thread and report progress as it occurs
Task.Run(async () => await this.SearchByStringAsync(p_searchParameter, _progressBarUpdate, _progressFileNameUpdate));
}
private async Task SearchByStringAsync(string p_searchParameter, IProgress<int> p_progressBar, IProgress<string> p_progressFileName)
{
await Task.Delay(1);
TextFileReader textFileReader = new TextFileReader();
LogSearchEventArguments logSearchEventArguments = null;
long _sessionloopCount = 0;
long _totalTextLinesCount = this.GetTotalSearchCount(p_searchParameter, SearchType.TextString);
// Get file names from SQL table
var _logFiles = DataOperations.LogFileSortableList(null);
foreach (var log in _logFiles)
{
// Format a file name to be read from the file system
string _fileName = log.Directory + "\\" + log.FileName;
p_progressFileName.Report(log.FileName);
// If we've raised an event for this file, then stop iterating over remaning text
if (logSearchEventArguments != null)
{
logSearchEventArguments = null;
break;
}
// Read in file contents from file system
List<string> _fileContents = textFileReader.ReadAndReturnStringList(_fileName);
long _fileTotalRecordCount = _fileContents.Count;
long _fileRecordCount = 0;
foreach (var _line in _fileContents)
{
if (_line.ToUpper().Contains(p_searchParameter.ToUpper()))
{
// Raise an event so search parameter and file name can be captured in another form
logSearchEventArguments =
new LogSearchEventArguments
(
"TextSearch", p_searchParameter, SearchType.TextString, true, log,
new DateTime(
Convert.ToInt32("20" + log.FileName.Substring(14, 2)),
Convert.ToInt32(log.FileName.Substring(16, 2)),
Convert.ToInt32(log.FileName.Substring(18, 2)))
);
// We found a match, so no further searching is needed in this log file,
// and it's been flagged in the DB, so raise the event to save search parameter and file name
// then break out of this loop to get the next file to search in.
this.OnSearchSucceeded(logSearchEventArguments);
break;
}
// These calcs are based on actual searches performed
_fileRecordCount++;
_sessionloopCount++;
p_progressBar.Report(Convert.ToInt32((_sessionloopCount * 100) / _totalTextLinesCount));
}
// Because we exit a search as soon as the 1st match is made, need to resynch all counts
// and update the progress bar accordingly
if (_fileRecordCount < _fileTotalRecordCount)
{
long _countDifference = _fileTotalRecordCount - _fileRecordCount;
// Add count difference to sessionLoopCount and update progress bar
_sessionloopCount += _countDifference;
p_progressBar.Report(Convert.ToInt32((_sessionloopCount * 100) / _totalTextLinesCount));
}
}
//Search is complete set Progress to 100% and report before exiting
p_progressBar.Report(100);
// Close the modal SearchForm and exit
this.Close();
}
}

I solved this problem but I'm still not certain of what caused it. I eliminated the method "private void SearchByText(string p_searchParameter)" and moved the code there into the btnSearch_Click event handler so I could call my background worker "SearchByStringAsync" directly from the button click event handler.
I also updated the EFCore NuGet Packages, which were version Net6.0 to version 6.0.4, because of single line of code in my Async background method, "var _logFiles = DataOperations.LogFileSortableList(null)".
That call returned a Sortable BindingList, using BindingList <T>. Between the NuGet updates and a minor change on a custom comparer method in my BindingList <T> class, the windows modal form now updates the ProgressBar and Label text as expected, and the form now responds immediately to user interaction.

Related

Iterate thread strategy for the FXML ProgressBar

I have coded myself into a corner here. In my FXML file, I declare a progress bar and an upload progress label (for a batch upload):
Program_view.fxml
<Label fx:id="uploadProgressLabel" layoutX="384" layoutY="579" text="Upload Progress">
<font>
<Font size="18" />
</font>
</Label>
...
<ProgressBar fx:id="uploadProgressBar" layoutX="185" layoutY="606" prefHeight="18" prefWidth="534" progress="0" />
Then I have a UI controller where I import all my elements from FXML:
UI_Controller.java
#FXML Label uploadProgressLabel;
#FXML ProgressBar uploadProgressBar;
Later in the UI Controller, there is a button whose action is to update the "upload" progress bar, and it doesn't. I've tried a few different thread / task strategies and none of them seem to work while they are running.
UI_Controller.java
#FXML
protected void distributeSetButtonClick() {
//In a previous version of this project using swing, I tossed this whole function into a new thread and that made the progress bar happy
//new Thread(() -> {
final boolean[] done = {false};
if (logTextArea.getText().equalsIgnoreCase("This is your console log, logs relating to uploading your set will appear here")) {
logTextArea.setText("");
}
//Upload each file and iterate label + counter
for (int i = 1; i <= uploadImages.size(); i++) {
System.out.println("Test: " + i);
uploadProgressLabel.setText("Uploading image " + i + "/" + uploadImages.size());
File f = uploadImages.get(i - 1);
mediaIds.add(UploadFile.uploadFile(f, logTextArea, i - 1, uploadImages.size()));
double currentProgress = (1.0 / uploadImages.size()) * i;
uploadProgressBar.setProgress(currentProgress);
}
uploadProgressLabel.setText("Completed uploading: " + uploadImages.size() + " images");
String areaUpdate = filesSelectedTextArea.getText();
if (mediaIds.size() == uploadImages.size()) {
areaUpdate += "\r\n\r\n All Images uploaded successfully";
} else {
areaUpdate += "\r\n\r\n One or more files had an error while uploading";
}
filesSelectedTextArea.setText(areaUpdate);
}
...
My question is, how can I get the progress bar / label to update while they are on the main thread? When I try moving them off the main thread I get an error about them not being on the JavaFX thread. I've also tried moving the logic over into a task, which looked like this (and then had a run of the task on the main thread) also to no avail:
Tasker.java
public static Task<Void> updateProgressBar(ProgressBar p, double value) {
Task<Void> task = new Task<Void>() {
#Override
protected Void call() throws Exception {
p.setProgress(value);
return null;
}
};
p.progressProperty().bind(task.progressProperty());
return task;
}
Some guidance would be appreciated.
Like most other UI toolkits, JavaFX is single threaded, with the FX Application Thread being responsible for processing user events and rendering the UI. This means:
You must not update UI controls from a background thread. JavaFX will throw IllegalStateExceptions in many (though not all) cases if you do this. (Note that Swing doesn't throw exceptions; it just leaves you vulnerable to arbitrary failure at some indeterminate point.)
Long-running processes (such as your file upload) must not be run on the FX Application Thread. Since this thread is responsible for rendering the UI and processing user events, no UI updates (such as updating the label and progress bar) will be possible until the long-running process is complete. Additionally, the UI will be unresponsive during this time.
You should use a Task to implement the long running process, and run the task on a background thread. The Task has thread-safe update methods which will update its properties (such as progress and message) on the FX Application Thread, so you can safely bind properties of UI elements to these properties. It also has onSucceeded and onFailed handlers, which are also executed on the FX Application thread. The onSucceeded handler can access any return value from the task.
So your code should look something like:
#FXML
protected void distributeSetButtonClick(){
//In a previous version of this project using swing, I tossed this whole function into a new thread and that made the progress bar happy
Task<String> task = new Task<>() {
#Override
protected String call() throws Exception {
final boolean[] done = {false};
//Upload each file and iterate label + counter
for (int i = 1; i <= uploadImages.size(); i++) {
System.out.println("Test: " + i);
updateMessage("Uploading image " + i + "/" + uploadImages.size());
File f = uploadImages.get(i - 1);
mediaIds.add(UploadFile.uploadFile(f, logTextArea, i - 1, uploadImages.size()));
updateProgress(i, uploadImages.size());
}
if (mediaIds.size() == uploadImages.size()) {
return "All Images uploaded successfully";
} else {
return "One or more files had an error while uploading";
}
}
};
if (logTextArea.getText().equalsIgnoreCase("This is your console log, logs relating to uploading your set will appear here")) {
logTextArea.setText("");
}
task.setOnSucceeded(event -> {
filesSelectedTextArea.append("\n\n"+task.getValue());
uploadProgressLabel.setText("Completed uploading: " + uploadImages.size() + " images");
});
uploadProgressLabel.textProperty().unbind()
uploadProgressLabel.textProperty().bind(task.messageProperty());
uploadProgressBar.progressProperty().unbind();
uploadProgressBar.progressProperty().bind(task.progressProperty());
Thread thread = new Thread(task);
thread.setDaemon(true);
thread.start();
}

Multiple Backgroundworker threads in a single Backgroundworker thread

I have an issue here with multi-threading. Tried using both backgroundworker and threads.
Objective: Select an item from a combo-box and click a button to trigger multiple backup file restores into the MSSQL Server. The intention is to popup as many popups as there are Backup files to restore. The main window starts a backgroundworker, showing the overall progress, while the child threads result in non-modal child popups representing each restoration process with progress. This works fine if run serially (without threads/backgroundworker). My intention is to run a bunch of parallel popups, so that the restoration is much more quick.
Problem Statement: My intention is to run a bunch of parallel popups, so that the restoration is much more quick instead of running serially. While trying to debug, its a chaotic break-point show that the Visual Studio shows up (perhaps to represent multiple threads in parallel). But the ultimate goal is not being achieved. Can anybody help me in this regard?
- Code Extracts
Here is the code extract, which I've done and this works in a serial fashion. But as soon as I put the code for multi-threading, nothing works. Only the popups appear, but no processing happens.
This is the button click event, which starts the whole process
private void btnSnapshot_Click(object sender, EventArgs e)
{
this.SetPanelEnabledProperty(false); // Disable All Controls on the main window
// Select the text against which the DATABASE Backup Files are to be picked
// Start the main background worker process, which in turn will trigger few other child threads
BGWrk.RunWorkerAsync(2000);
BGWrk.DoWork += new DoWorkEventHandler(BGWrk_DoWork);
BGWrk.ProgressChanged += new ProgressChangedEventHandler(BGWrk_ProgressChanged);
BGWrk.RunWorkerCompleted += new RunWorkerCompletedEventHandler
(BGWrk_RunWorkerCompleted);
BackgroundWorker helperBW = sender as BackgroundWorker;
BGWrk.WorkerReportsProgress = true;
BGWrk.WorkerSupportsCancellation = true;
}
private void BGWrk_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker helperBW = sender as BackgroundWorker;
int arg = (int)e.Argument;
//Call the method that invokes the popup window instances in a loop, for each file found for the SnapShotName selected in the combobox
ParallelRestoreSnapshot();
if (helperBW.CancellationPending)
{
e.Cancel = true;
}
}
// This method will create backgroundworker instances and in turn the Do_Work events of those backgroundworkers will call the popup against each backup file.
private bool ParallelRestoreSnapshot()
{
FileOperations FIO = new FileOperations();
if (SelectedSnapShot != null && SelectedSnapShot != "None")
{
string[] sBakFiles;
sBakFiles = FIO.GetListOfBackupFiles(sEntireBackupFilePath);
try
{
progressPopupsList = new List<FrmProgressPopup>();
for (int aIndex = 0; aIndex < sBakFiles.Length; aIndex++)
{
BackgroundWorker bgPopups = new BackgroundWorker();
BAKFileName = sBakFiles[aIndex];
bgPopups.RunWorkerAsync(2000);
bgPopups.DoWork += new DoWorkEventHandler(bgPopups_DoWork);
bgPopups.ProgressChanged += new ProgressChangedEventHandler(bgPopups_ProgressChanged);
bgPopups.RunWorkerCompleted += new RunWorkerCompletedEventHandler
(bgPopups_RunWorkerCompleted);
bgPopups.WorkerReportsProgress = true;
bgPopups.WorkerSupportsCancellation = true;
}
retVal = true;
}
catch (Exception exc)
{
MessageBox.Show("Error while Restoring: " + exc.Message, "Exception encountered", MessageBoxButtons.OK, MessageBoxIcon.Error);
//goto NextDB;
return false;
}
}
FIO = null;
return retVal;
}
// This DoWork event calls a method GeneratePopupInstances which makes copies of a window, which is shown as non-modal
private void bgPopups_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker helperBW = sender as BackgroundWorker;
int arg = (int)e.Argument;
//BackgroundProcessLogicMethod(helperBW, arg);
GeneratePopupInstances();
if (helperBW.CancellationPending)
{
e.Cancel = true;
}
}

Event firing continuously

I wrote a method which changes backcolor of the rows before painting gridview in devexpress. It works fine but I realized that my code begins slowing down. Then I've found that the event firing continuously. It never stops. How can I handle this? Is there any way to stop firing event manually after gridview painted or should I try to solve this problem with an another event or another method???
Here is my event:
private void gvStep_CustomDrawCell(object sender, DevExpress.XtraGrid.Views.Base.RowCellCustomDrawEventArgs e)
{
try
{
DataRowView drw = (DataRowView)gvStep.GetRow(e.RowHandle);
byte actionTypeID = (byte)drw.Row["ActionType"];
//string colorCode = (new DivaDs()).GetBackColor(actionTypeID);
string colorCode = divaDs.GetBackColor(actionTypeID);
Color backColor = ColorTranslator.FromHtml(colorCode);
e.Appearance.BackColor = backColor;
}
catch (Exception ex)
{
XtraMessageBox.Show(ex.Message);
}
}
public string GetBackColor(byte actionTypeID)
{
string color = string.Empty;
using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings[DivaSqlSiteConnString].ConnectionString))
{
using (SqlCommand cmd = new SqlCommand(#"Select BackColor from ActionTypes where ID = #actionTypeID"))
{
SqlParameter param = new SqlParameter("#actionTypeID", actionTypeID);
cmd.Parameters.Add(param);
cmd.Connection = conn;
conn.Open();
color = cmd.ExecuteScalar().ToString();
conn.Close();
}
}
return color;
}
My best guess is that some part of your code is just really slow.
The event only fires for each visible cell in the grid. If you attempt to debug the event, focus will shift to the debugger, and when you return to the application the cells need to be redrawn, causing the event to fire again, thus giving the impression that the event fires continuously. It does not, however.
Here are some pointers to improve performance:
You are constructing a new DivaDs every time the event fires
Instead, consider reusing the same instance of the class as a member variable
What happens in the constructor?
Take a closer look at the GetBackColor method or ColorTranslator.FromHtml and see if any modifications can be made to improve performance.
Update
It appears you are querying the database for each cell in the grid. This is a really bad idea.
A simple solution would be to preload all ActionTypes and their background colors (or at least the subset of ActionTypes that is displayed in the grid) before setting the grid's data source.
// member variable
private Dictionary<byte, Color> actionTypeColorDict;
void BuildActionTypeColorDictionary()
{
string connectionString = ConfigurationManager
.ConnectionStrings[DivaSqlSiteConnString].ConnectionString;
using (SqlConnection conn = new SqlConnection(connectionString))
using (SqlCommand cmd = conn.CreateCommand())
using (SqlDataAdapter adapter = new SqlDataAdapter(cmd))
{
// load all action type IDs and corresponding background color:
cmd.CommandText = #"SELECT ActionTypeID, BackColor FROM ActionTypes";
DataTable actionTypeTable = new DataTable();
adapter.Fill(actionTypeTable);
// build a dictionary consisting of action type IDs
// and their corresponding colors
actionTypeColorDict = actionTypeTable.AsEnumerable().ToDictionary(
r => r.Field<byte>("ActionTypeID"),
r => ColorTranslator.FromHtml(r.Field<string>("ColorCode")));
}
}
Call the BuildActionTypeColorDictionary method before setting the data source of the grid. In the RowStyle or CustomDrawCell events, use the new dictionary member to determine the background color. See the following modified version of your RowStyle code:
private void gvStep_RowStyle(object sender,DevExpress.XtraGrid.Views.Grid.RowStyleEventArgs e)
{
try
{
DataRow row = gvStep.GetDataRow(e.RowHandle);
if (row == null)
return;
byte actionTypeID = row.Field<byte>("ActionImage");
// look up color in the dictionary:
e.Appearance.BackColor = actionTypeColorDict[actionTypeID];
}
catch (Exception ex)
{
XtraMessageBox.Show(ex.Message);
}
}
How do you know it's firing continuously? Are you debbuging?
This code runs whenever the grid is redrawn, meaning whenever the form gets focus.
This event runs for each cell - so it will run quite a few times.
If you put a break-point in this event you'll never get out of it. It will break, you will debug, when it's done it will return focus to the form - causing the form to be redrawn using this event and the break-point is reached again.
And just a side note - Whenever I use that event I have to put e.Handled = true; in the code so that the cell isn't "drawn" by anyone but me :)
Finally, I found it. RowStyle event only fires same time with gridview's row count
private void gvStep_RowStyle(object sender, DevExpress.XtraGrid.Views.Grid.RowStyleEventArgs e)
{
try
{
DataRowView drw = (DataRowView)gridView1.GetRow(e.RowHandle);
if (drw == null)
return;
byte actionTypeID = (byte)drw.Row["ActionImage"];
string colorCode = divaDs.GetBackColor(actionTypeID);
Color backColor = ColorTranslator.FromHtml(colorCode);
e.Appearance.BackColor = backColor;
}
catch (Exception ex)
{
XtraMessageBox.Show(ex.Message);
}
}

Shared Variable in C# .Net MVC Thread

I am uploading videos to server in .Net MVC application. Since it is takes some time I am doing in a background thread. On the other hand I am tracking the upload progress and display it to the user.
Here is my code
public class MyController : Controller
{
long chunkSize = 256 * 1024;
private static string _progress = ""; //if I make nonstatic it fails
//..........
//Some codes here
//..........
//Upload file Request
public ActionResult VideoUploader()
{
var client = (VimeoClient)Session["client"];
Ticket t = client.vimeo_videos_upload_getTicket();
string path = #"E:\d2.MOV"; //Hardcoded value for testing purposes
new Thread(() => Transfer(client, t, path)).Start(); //Push file to server in background
return PartialView("_progress", "Preparing to upload...");
}
//Worker Thread
private void Transfer(VimeoClient client, Ticket t, string path)
{
FileInfo UploadFile = new FileInfo(path);
int chunks = (int)Math.Floor(Convert.ToDouble(UploadFile.Length / chunkSize));
for (int i = 0; i <= chunks; i++)
{
string output = client.PostVideo(t, i, path,(int) chunkSize);
var v = client.vimeo_videos_upload_verifyChunks(t);
double percentage = (Convert.ToDouble(i + 1) / (chunks + 1)) * 100;
_progress = percentage.ToString() + "%"; // value sharing between two actions
}
string name = client.vimeo_videos_upload_complete(path, t);
client.vimeo_videos_embed_setPreset(client.Token, "200772", name);
}
//This method is calling from front end using jQuery to display progress
public ActionResult Progress()
{
//returning updated shared "_progress" varibal
return Json(_progress, JsonRequestBehavior.AllowGet);
}
}
My problem is when I made "_progress" nonstatic variable(private static _progress) it is not working. Value is always empty.
What I trying to do is share _progress variable between main thread and new thread. But it is failing when it nonstatic.
Please help me to correct. If you have a better option please share
Thanks in advance
You should be returning some sort of "operation ID" to the client as part of the original response. The Progress method should then take this operation ID as a parameter. The upload service should then store its progress against that operation ID (e.g. in a database, to allow you to scale horizontally).
Basically, you should imagine the situation where there are multiple transfers involved - you need some way of getting the progress for the right transfer.
Instance of MyController is created per request.
After you spin the worker thread you return the page to the client and that instance is gone.
You can pass this(i.e. the controller instance) to the Transfer method like this
new Thread(() => Transfer(this, client, t, path)).Start();
and then access the _progress variable in the Transfer method like this
private void Transfer(MyController controller,
VimeoClient client, Ticket t, string path)
{
controller._progress
the page is already returned, so you'll just be able to access the instance _progress field, but then how do you update the client?
you can either
return in the view an animated gif and make a periodical request to the server with javascript OR
use a library like SignalR to 'push' the progress update to the client

How do I block access to a method until animations are complete

I have a Silverlight app. that has a basic animation where a rectangle is animated to a new position. The animation consists of two DoubleAnimation() - one transforms the X, the other transforms the Y. It works OK.
I basically want to block any other calls to this animate method until the first two animations have completed. I see that the DoubleAnimation() class has a Completed event it fires but I haven't been successful in constructing any kind of code that successfully blocks until both have completed.
I attempted to use Monitor.Enter on a private member when entering the method, then releasing the lock from one of the animations Completed event, but my attempts at chaining the two events (so the lock isn't released until both have completed) haven't been successful.
Here's what the animation method looks like:
public void AnimateRectangle(Rectangle rect, double newX, double newY)
{
var xIsComplete = false;
Duration duration = new Duration(new TimeSpan(0, 0, 0, 1, 350));
var easing = new ElasticEase() { EasingMode = EasingMode.EaseOut, Oscillations = 1, Springiness = 4 };
var animateX = new DoubleAnimation();
var animateY = new DoubleAnimation();
animateX.EasingFunction = easing;
animateX.Duration = duration;
animateY.EasingFunction = easing;
animateY.Duration = duration;
var sb = new Storyboard();
sb.Duration = duration;
sb.Children.Add(animateX);
sb.Children.Add(animateY);
Storyboard.SetTarget(animateX, rect);
Storyboard.SetTargetProperty(animateX, new PropertyPath("(Canvas.Left)"));
Storyboard.SetTarget(animateY, rect);
Storyboard.SetTargetProperty(animateY, new PropertyPath("(Canvas.Top)"));
animateX.To = newX;
animateY.To = newY;
sb.Begin();
}
EDIT (added more info)
I ran into this initially because I was calling this method from another method (as it processed items it made a call to the animation). I noticed that the items didn't end up where I expected them to. The new X/Y coordinates I pass in are based on the items current location, so if it was called multiple times before it finished, it ended up in the wrong location. As a test I added a button that only ran the animation once. It worked. However, if I click on the button a bunch of times in a row I see the same behavior as before: items end up in the wrong location.
Yes, it appears Silverlight animations are run on the main UI thread. One of the tests I tried I added two properties that flagged whether both animations had completed yet. In the AnimateRectange() method I checked them inside of a while loop (calling Thread.Sleep). This loop never completed (so it's definitely on the same thread).
So I created a queue to process the animations in order:
private void ProcessAnimationQueue()
{
var items = this.m_animationQueue.GetEnumerator();
while (items.MoveNext())
{
while (this.m_isXanimationInProgress || this.m_isYanimationInProgress)
{
System.Threading.Thread.Sleep(100);
}
var item = items.Current;
Dispatcher.BeginInvoke(() => this.AnimateRectangle(item.Rect.Rect, item.X, item.Y));
}
}
Then I call my initial routine (which queues up the animations) and call this method on a new thread. I see the same results.
As far as I am aware all of the animations in Silverlight are happening on the UI thread anyway. I am guessing that only the UI thread is calling this animation function anyway, so I am not sure that using locking will help. Do you really want to be blocking the entire thread or just preventing another animation from starting?
I would suggest something more like this:
private bool isAnimating = false;
public void AnimateRectangle(Rectangle rect, double newX, double newY)
{
if (isAnimating)
return;
// rest of animation code
sb.Completed += (sender, e) =>
{
isAnimating = false;
};
isAnimating = true;
sb.Begin();
}
Just keep track of whether or not you are currently animating with a flag and return early if you are. If you don't want to lose potential animations your other option is to keep some kind of a queue for animation which you could check/start when each animation has completed.
This question really peaked my interest. In fact I'm going to include it in my next blog post.
Boiling it down, just to be sure we are talking about the same thing, fundementally you don't want to block the call to AnimateRectangle you just want to "queue" the call so that once any outstanding call has completed its animation this "queued" call gets executed. By extension you may need to queue several calls if a previous call hasn't even started yet.
So we need two things:-
A means to treat what are essentially asynchronous operations (sb.Begin to Completed event) as a sequential operation, one operation only starting when the previous has completed.
A means to queue additional operations when one or more operations are yet to complete.
AsyncOperationService
Item 1 comes up in a zillion different ways in Silverlight due to the asynchronous nature of so many things. I solve this issue with a simple asynchronous operation runner blogged here. Add the AsyncOperationService code to your project.
AsyncOperationQueue
Its item 2 that really took my interest. The variation here is that whilst an existing set of operations are in progress there is demand to add another. For a general case solution we'd need a thread-safe means of including another operation.
Here is the bare-bones of a AsyncOperationQueue:-
public class AsyncOperationQueue
{
readonly Queue<AsyncOperation> myQueue = new Queue<AsyncOperation>();
AsyncOperation myCurrentOp = null;
public void Enqueue(AsyncOperation op)
{
bool start = false;
lock (myQueue)
{
if (myCurrentOp != null)
{
myQueue.Enqueue(op);
}
else
{
myCurrentOp = op;
start = true;
}
}
if (start)
DequeueOps().Run(delegate { });
}
private AsyncOperation GetNextOperation()
{
lock (myQueue)
{
myCurrentOp = (myQueue.Count > 0) ? myQueue.Dequeue() : null;
return myCurrentOp;
}
}
private IEnumerable<AsyncOperation> DequeueOps()
{
AsyncOperation nextOp = myCurrentOp;
while (nextOp != null)
{
yield return nextOp;
nextOp = GetNextOperation();
}
}
}
Putting it to use
First thing to do is convert your existing AnimateRectangle method into a GetAnimateRectangleOp that returns a AsyncOperation. Like this:-
public AsyncOperation GetAnimateRectangleOp(Rectangle rect, double newX, double newY)
{
return (completed) =>
{
// Code identical to the body of your original AnimateRectangle method.
sb.Begin();
sb.Completed += (s, args) => completed(null);
};
}
We need to hold an instance of the AsyncOperationQueue:-
private AsyncOperationQueue myAnimationQueue = new AsyncOperationQueue();
Finally we need to re-create AnimateRectangle that enqueues the operation to the queue:-
public void AnimateRectangle(Rectangle rect, double newX, double newY)
{
myAnimationQueue.Enqueue(GetAnimateRectangleOp(rect, newX, newY)
}

Resources