Slow updating of UI with WPF and Oxyplot - c#

What I'm trying to accomplish here is I want grab some data from a CSV file that I copy from one folder and put into a temp folder (so I don't tamper with the original).
Then I want to read in all the data from the CSV file and plot the last 2000 data points on a graph using a Scatter Series in Oxyplot (I want this to check for new data every 200 ms so I used a Dispatcher Timer). The issue I'm having is that the first few updates to the plot look great and it plots exactly how I want it to... however, after maybe 12 updates the graph does not update, or updates extremely slowly causing the UI to become unresponsive.
Below is my code for this part of the project...
public MainWindow()
{
viewModel = new View_Model.MainWindowModel();
DataContext = viewModel;
CompositionTarget.Rendering += CompositionTargetRendering;
InitializeComponent();
}
private void AutoUpdateGraph() //begins when a check box IsChecked = true
{
sw.Start();
System.Windows.Threading.DispatcherTimer dispatcherTimer = new System.Windows.Threading.DispatcherTimer();
dispatcherTimer.Tick += new EventHandler(dispatcherTimer_Tick);
dispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 200);
dispatcherTimer.Start();
}
private void dispatcherTimer_Tick(object sender, EventArgs e)
{
SleeveData sd = new SleeveData(); //class to deal with the data
FileManagement fm = new FileManagement(); //class to manage the files
string date = fm.GetDate(); //get the current date to use in finding the
//most recent CSV file
_newPath = fm.InitialFileSetup(date); //create a new path for the temp file
fm.RewriteFile(_newPath); //write the temp file to the temp path
if (fm.IsFileLocked(_newPath) == false) //checking if the file is being written to
{
IEnumerable<SleeveData> newSD = sd.GetMostRecentCSVData(_newPath); //getting the latest data from the temp file
viewModel.LoadData(newSD); //updating the data on the graph
}
}
And here is the LoadData method in the MainWindowModel...
public void LoadData(IEnumerable<SleeveData> newData)
{
var scatterSeries1 = new OxyPlot.Series.ScatterSeries
{
MarkerSize = 3,
Title = string.Format("Sleeve Data"),
};
int j = 0;
var zeroBuffer = new List<float>(new float[2000]);
var fDiaDataBuffer = zeroBuffer.Concat((newData.Select(x => x.fDiameter).ToList())).ToList();
var iDiaDataBuffer = zeroBuffer.Concat((newData.Select(x => x.IDiaMax).ToList())).ToList();
for (int i = fDiaDataBuffer.Count - 2000; i <= fDiaDataBuffer.Count - 1; i++)
{
scatterSeries1.Points.Add(new ScatterPoint(j, fDiaDataBuffer[i]));
j++;
}
PlotModel.Series.Clear();
PlotModel.Series.Add(scatterSeries1);
}
I'm doing some funky stuff to get the latest 2000 points and add them to the graph.
Maybe I need to consider a background worker?
I might be going about this all wrong, and if I am I would love to know in which direction I should go! I'm fairly new to coding projects this large and ones that must run real time. Please go easy on me :) and thanks in advance.

Some suggestions:
Use a Stopwatch in your LoadData() method to see how long it's taking. You don't say how much data is in these files, but some of the Linq stuff looks like it could benefit from some optimisation - there are lots of ToLists() and repeated Selects on the same set of data. Something like this:
var sw = new Stopwatch();
sw.Start();
... do your stuff
Debug.WriteLine(sw.ElapsedMilliseconds);
You could also try a stopwatch in the timer delegate, to see how long an entire "cycle" takes, just in case it's taking longer than 200ms. I don't know if the DispatcherTimer can suffer from re-entrancy, where the timer fires again before the previous "tick" has completed, which would degrade performance.
The problem might lie with your charting component. I've had similar issues when working with large WPF DataGrids - creating the data isn't particularly taxing - it's the rendering that takes the time. At the end of LoadData() I see you clear down the chart series then repopulate it. I've found that doing this with the data source for a large DataGrid creates a "double-whammy", as it renders after the Clear(), then renders again after repopulating the data. I'm not familiar with OxyPlot but see if you can find an alternative approach, if feasible, e.g. re-using the series rather than clearing down and adding again.
If the poor performance does turn out to be OxyPlot, is purchasing a different chart component an option? I can thoroughly recommend SciChart, which we've been using for various high performance/high volume scientific charting applications (I'm not affiliated to them btw!).

Related

Assigning a value to Label.Text eventually results in StackOverflowException

I am building a dashboard style Windowsform, I have various timers to update the controls on the form, and all are working great, except one, the only one I am using a Label Control as the indicator.
I have various System.Timers to update the data of the indicators all stored in classes that run ever 5 minutes or so, then another timer set to update the GUI that runs every second. For some reason this code:
l_LastShipment.Text = GlobalStatic.fulfillmentInd.Caption;
within the GUI Update eventually errors in a StackOverflowException, usually after a couple of hours. fulfillmentInd.Caption is just a string variable in the class, and at the time of the error contains the proper data (Usually "0:01" or something similar).
Originally the GUI Timer was a System.Timer but was afraid the Invoke to update the Label was were the error was coming from, so switched the GUI timer to a Windows.Forms.Timer so it didn't require the Invoke, yet the error still occurs.
There is no recursion taking place, and I even monitored the time it takes the Update GUI function to run and it is always less than 1/10th of a second, even when the error occurs.
Here is the trimmed GUI Refresh function:
private void guiHandleTimer(object sender, EventArgs e)
{
//Refresh the Indicators
//Stopwatch
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
//Compact indicator
if (GlobalStatic.compactInd.Changed())
{
pb_Compact.Value = GlobalStatic.compactInd.value;
pb_Compact.Caption = GlobalStatic.compactInd.Caption;
pb_CompactTT.SetToolTip(pb_Compact, GlobalStatic.compactInd.tooltip);
GlobalStatic.compactInd.Reset();
}
//
//Other Indicators removed for readability
//
//Fulfillment Indicator
if (GlobalStatic.fulfillmentInd.Changed())
{
if (GlobalStatic.fulfillmentInd.value <= 59)
{
//Within an Hour, Light Yellow
p_fulfillment.BackColor = Color.LightYellow;
}
else if (GlobalStatic.fulfillmentInd.value <= 300)
{
//Between an hour and 5 hours, Change to Green
p_fulfillment.BackColor = Color.Green;
}
else
{
//Over 5 hours old, Grey
p_fulfillment.BackColor = Color.LightGray;
}
l_LastShipment.Text = GlobalStatic.fulfillmentInd.Caption; <-------This is the line that generates the StackOverflow ----->
ToolTip test = new ToolTip();
test.SetToolTip(p_fulfillment, GlobalStatic.fulfillmentInd.tooltip);
test.SetToolTip(l_LastShipment, GlobalStatic.fulfillmentInd.tooltip);
GlobalStatic.fulfillmentInd.Reset();
}
stopwatch.Stop();
TimeSpan t = stopwatch.Elapsed;
if(TimeSpan.Compare(largestTime,t)==-1)
{
//largestTime is shorter than t, change largestTime
largestTime = t;
}
T_GUI.Text = largestTime.ToString(#"hh\:mm\:ss\:fff");
}
Here is the Timer Setup, called during Form Load, the only place that GUIHandleTimer is mentioned in code:
//GUI Refresh Timer
System.Windows.Forms.Timer guiTimer = new System.Windows.Forms.Timer();
guiTimer.Interval = 1000;
guiTimer.Tick += new EventHandler(guiHandleTimer);
I solved the StackOverflow. The reason it took so long for me to figure it out and finally to ask here, is that Visual Studio was always code-breaking on the Label.Text line highlighted in my original question. Unfortunately, that was not the line causing the StackOverflow, it was the next line that was causing it.
ToolTip test = new ToolTip();
For some reason I didn't move that New ToolTip() out of the Handler after I was done fixing my ToolTips, so I was overloading new ToolTips into the Stack every second. Moving that line out of the function fixed the error.
The weird thing is that wasn't where Visual Studio was marking the StackOverflow error, causing much pain in attempting to resolve it.

C# real time chart desktop application too slow

I am taking over a C# code from someone who implemented a desktop application to read real time data from the Serial Port and displaying it in charts (using the Chart Class).
The code seems to be working, but is very slow. It seems to be updating the chart around once in 4 seconds (0.25Hz). However, I can see that it is multi-threaded and has no delay commands, so I don't understand why it is running so slow. Could the updating of charts slow it down? Ideally I would like to achieve 1000 data points per second (1kHz), displaying it in real time and saving it to the hard disk, where the size of each data point is about 30 bytes.
I spent few days understanding the code, but it's too cumbersome to follow, all written in a single file, with no comments. Is my goal of reading 1000 data points per second realistic/achievable?
I'm also considering to re-write the code (as opposed to trying to fix it), considering it's only about 2,500 lines long. Any tips would be appreciated. Also, if I rewrite the code, what language might be better for this application?
I developed some code where I got significant performance improvement, it may work for you. Here is what I did-
Step 1: I would first identify, which one is the bottle neck, drawing/rendering of the chart
or serial port
Step 2: If you find its the rendering-- then add this in your form/chart setup, it will draw much faster. But first double check to make sure you're not in remote desktop mode.
<!-- language: c# -->
// No double buffering for remote, only local
public bool OptimizeOfLocalFormsOnly(System.Windows.Forms.Control chartControlForm)
{
if (!System.Windows.Forms.SystemInformation.TerminalServerSession)
{
SetUpDoubleBuffer(chartControlForm);
return true;
}
return false;
}
public static void SetUpDoubleBuffer(System.Windows.Forms.Control chartControlForm)
{
System.Reflection.PropertyInfo formProp =
typeof(System.Windows.Forms.Control).GetProperty("DoubleBuffered", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
formProp.SetValue(chartControlForm, true, null);
}
I assume you using winform application :
Use serialPort component :
Configure its properties : (BaudRate, DataBits, StopBits, Parity ...)
Make use of its event (DataReceived) to collect your inputs.
You can send commands in a loop and collect the inputs/drawing them on chart component roughly like :
while(/*some condition*/)
{
serialPort.Write(/* your command */);
// you have to think of response time
// so implement something that waits a bit before calling the port again
// I would do it using timers
int tick= Environment.TickCount;
while(Environment.TickCount - tick <= 100) // wait 100 milliseconds
{
Application.DoEvents();
}
}
// collect the data as:
private void serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
// use according to your needs, for example
string data = "";
int readByte;
int available;
available = serialPort.BytesToRead;
if(available >= 30) // 30 bytes as stated in your question
{
for(int i=0; i<30; i++)
{
readByte = serialPort.ReadByte();
data += String.Format("{0:2X}", readByte);
}
// you can call some methods to save/display your collected data from here
save_to_file(data);
display_to_chart(data);
}
}
I developed a similar app where I was displaying 16charts * 256 samples/sec. Storing the data in a buffer and creating a separate thread for updating the charts worked for me.
When new data is read, data is stored in a list or array. Since it is real-time data, the timestamps are also generated here. Using the sample rate of the data acquired: timeStamp = timeStamp + sampleIdx/sampleRate;
public void OnDataRead(object source, EEGEventArgs e)
{
if ((e.rawData.Length > 0) && (!_shouldStop))
{
lock (_bufferRawData)
{
for (int sampleIdx = 0; sampleIdx < e.rawData.Length; sampleIdx++)
{
// Append data
_bufferRawData.Add(e.rawData[sampleIdx]);
// Calculate corresponding timestamp
secondsToAdd = (float) sampleIdx/e.sampleRate;
// Append corresponding timestamp
_bufferXValues.Add( e.timeStamp.AddSeconds(secondsToAdd));
}
}
Then, create a thread that sleeps every N ms (100ms is suitable for me for a 2 seconds display of data, but if I wanna display 10 seconds, I need to increase to 500ms of sleep time for the thread)
//Create thread
//define a thread to add values into chart
ThreadStart addDataThreadObj = new ThreadStart(AddDataThreadLoop);
_addDataRunner = new Thread(addDataThreadObj);
addDataDel += new AddDataDelegate(AddData);
//Start thread
_addDataRunner.Start();
And finally, update the charts and make the thread sleep every N ms
private void AddDataThreadLoop()
{
while (!_shouldStop)
{
chChannels[1].Invoke(addDataDel);
// Sleeep thread for 100ms
Thread.Sleep(100);
}
}
The AddData method, simply reads the X (timestamp) and Y values stored in the buffer and add its to the charts using ptSeries.Points.AddXY(xValue, yValue)

How do you update a datagrid (or any other UI control) continuously without freezing the UI?

In a WinForms application, I have a datagrid that is associated to a datasource. As and when data comes in through a background thread the dataset needs to be updated which in turn automatically updates the datagrid. Now, the updates can be in the order of say, 7000 updates per 20 seconds.
The probelm is that the UI hangs when such an update happens because it has to happen on the main thread. Is there a know solution for this problem?
Generally, how can you design highly performant enterprise applications in WinForms where the UI is being updated continuously without the application freezing?
Adding a scenario to explain this:
Consider this scenario. You have a tree view which you are using to represent some hierarchical data. Now the data update on the tree is async. The server can publish one or 1000 updates at the same same time. The update can be a modification of an existing item or addition of new nodes. The point to be noted is that the update cannot be delayed. The nodes represent a real time entity somewhere. Delaying the update will give the user the perception that the event itself was delayed. So this cant be done. If it was possible (from the business logic point of view) I would have done it a long time back.
There is a key point here: All the data need not be visible at the same time.
So that people dont suggest this anymore:
Adding a background worker thread WILL NOT HELP because the thread has to switch to the main thread to perform the update. The worker thread will make no difference.
You cannot, unless you want to use DirectX.
Windows Forms is not designed for real-time information display. As many others have pointed out, you can get very close, but due to the way the Windows message loop works, you absolutely cannot guarantee that what is on the screen will be "real-time", even if you create a Timer that ticks at 60hz. Even if you do this in an event-driven manner, Windows will still queue up a WM_PAINT message that will inevitably be late if you are looking for real-time display.
If you truly want a extremely-close-to-realtime display, you will need to implement something similar to a Game Loop.
For an explanation of why the Windows Message loop won't work for real-time display, and what a game loop is, see:
http://www.mvps.org/directx/articles/writing_the_game_loop.htm
A computer game cannot have any perceivable delay, so most computer games try to optimize performance so that they approach a framerate at or above the holy grail of 60hz. (Movies are only projected at 24hz, do you perceive them to be "delayed"?)
Writing an application with a real-time display is non-trivial and I would highly suggest considering compromising with what Windows provides in any of the following ways:
Create a timer that queues up screen updates at an acceptable rate (10 or more times per second). The user will not perceive the event as delayed, because a user cannot perceive delays that occur at a small fraction of a second.
Raise an event when the underlying data changes and let Windows to decide when to update the display (this will virtually always be acceptable).
If possible, come up with an alternate display that is not grid-based. Perhaps a scrolling console, or some other interface that displays the relevant information without overwriting old information. This may not be applicable, but coming up with another interface idea is often a good approach when the interface you want won't work.
If you really, really still want a very high-performance user interface, and write a game loop, you can do so in C# and paint a grid on your own to a DirectX surface. Once you get the hang of DirectX, drawing a grid is fairly easy, it's just a bunch of lines. With this approach you will avoid dealing with the Windows message loop, and potentially approach real-time performance.
Here's a great tutorial on how to use DirectX and how to render on a Windows form:
http://www.godpatterns.com/2005/02/using-directx-and-c-sharp-to-create.html
In your comment you say that your heavy processing is reporting progress very often and you can't drop any report (because the report is real data that needs showing).
What you should do is implement (double) buffering, report progress to a buffer and only synchronize the buffer with GUI every once in a while.
Pseudocode follows:
DataGrid Grid; // This displays the data
List<object> DataBuffer; // Frequent updates are performed on this list
void BackgroundThreadLoop()
{
while(true) // This loop iterates 7000 times in 20 seconds
{
var result = DoSomeHeavyCalculations();
// Depending on the nature of the result, you can either just add it to list
// or perhaps modify existing entries in the list in some way.
DataBuffer.Add(result); // The simple case
PerformSomeUpdating(DataBuffer, result); // The complicated case
}
}
Timer RefreshTimer;
override void OnLoad()
{
RefreshTimer = new Timer();
RefreshTimer.Interval = 500; // easy to experiment with this
RefreshTimer.Tick += (s, ea) => DrawBuffer(DataBuffer);
}
void DrawBuffer(List<object> DataBuffer)
{
// This should copy DataBuffer and put it in the grid as fast as possible.
// How to do this really depends on how the list changes and what it contains.
// If it's just a list of strings:
Grid.DataSource = DataBuffer.ToList(); // Shallow copy is OK with strings
// If it's a list of some objects that have meaningful Clone method:
Grid.DataSource = DataBuffer.Select(o => o.Clone).ToList();
// If the number of elements is like constant and only some values change,
// you could use some Dictionary instead of List and just copy values.
}
If you give more precise info, I might be able to help further.
UPDATE
With the new details, I'd suggest buffering individual changes made to objects. The most general way of representing a change to some objects structure would be a function (perhaps parameterless Action). While receiving changes, you construct the update-functions modifying directly the view-bound data and store them in the buffer:
List<Action> UpdateBuffer;
void OnUpdateReceived(MyType objToModify, object newValue)
{
// The point is to make the lambda (below) as efficient as you can;
// finding the object and preparing the update should be done here, so that
// no time is wasted during redraw in the main thread.
UpdateBuffer.Add(() => objToModify.ApplyNewValueInSomeWay(newValue));
// some other method should be constructed to add data to the view, but you get the point
}
Now the DrawBuffer (name no longer fully adequate, but no matter) method would be easy:
void DrawBuffer()
{
List<Action> bufferCopy;
lock(UpdateBuffer) // the other thread should also lock the buffer for adding
{
bufferCopy = UpdateBuffer.ToList();
UpdateBuffer.Clear();
}
view.SuspendLayout();
foreach(Action a in bufferCopy)
a();
view.ResumeLayout();
}
Obviously I have not tried this exact solution, but it gives you the ability to control redraw frequency and redraw whole batches instead of single updates.
I just made an sample application, that will fill its internal list through a BackgroundWorker and the data will be displayed within a DataGridView. You can change the speed of the inserts to find out if it meets your requirements:
The most interesting part should be the code within the form itself:
public partial class FormMain : Form
{
private List<Person> _Persons;
private Random _Random;
private int _TimeoutBetweenInserts;
public FormMain()
{
InitializeComponent();
// Initialize our private fields
_Random = new Random();
_Persons = new List<Person>();
_TimeoutBetweenInserts = (int)numericUpDownTimeoutBetweenInserts.Value;
// Attach the list to the binding source and get informed on list changes.
personBindingSource.DataSource = _Persons;
personBindingSource.ListChanged += (sender, e) => labelDataGridViewCount.Text = _Persons.Count.ToString();
}
private void OnBackgroundWorkerDoWork(object sender, DoWorkEventArgs e)
{
var spinner = new SpinWait();
var worker = (BackgroundWorker)sender;
// Should we abort our adding?
while (!worker.CancellationPending)
{
// Create a new entry ...
var person = new Person();
person.Index = _Persons.Count;
person.Born = new DateTime(_Random.Next(1950, 2012), _Random.Next(1, 13), _Random.Next(1, 28));
person.FirstName = "Hello";
person.LastName = "World";
// ... and add it to the list
_Persons.Add(person);
// Do a little waiting ... (to avoid blowing out the list)
for (int i = 0; i < _TimeoutBetweenInserts; i++)
{
spinner.SpinOnce();
}
spinner.Reset();
}
}
private void OnBackgroundWorkerRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
// Stop the gui updater, cause the background worker also stopped.
timerGuiUpdater.Stop();
}
private void OnCheckBoxToggleWorkerCheckedChanged(object sender, EventArgs e)
{
// Update the "button" according to the state
checkBoxToggleWorker.Text = checkBoxToggleWorker.Checked ? "&Pause" : "&Start";
if (checkBoxToggleWorker.Checked)
{
if (!backgroundWorker.IsBusy)
{
// Start the gui updater and the background worker
timerGuiUpdater.Start();
backgroundWorker.RunWorkerAsync();
}
}
else
{
// Stop the background worker
backgroundWorker.CancelAsync();
}
}
private void OnNumericUpDownTimeoutBetweenInsertsValueChanged(object sender, EventArgs e)
{
// Update the internal value, to let it propagate into the background worker
_TimeoutBetweenInserts = (int)numericUpDownTimeoutBetweenInserts.Value;
}
private void OnTimerGuiUpdaterTick(object sender, EventArgs e)
{
// Tell the BindingSource it should inform its clients (the DataGridView)
// to update itself
personBindingSource.ResetBindings(false);
}
}
To let you access all these fields within the form, here comes the Designer.cs:
partial class FormMain
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.dataGridView = new System.Windows.Forms.DataGridView();
this.Index = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.lastNameDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.firstNameDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.bornDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.ageDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.personBindingSource = new System.Windows.Forms.BindingSource(this.components);
this.backgroundWorker = new System.ComponentModel.BackgroundWorker();
this.labelDataGridViewCountText = new System.Windows.Forms.Label();
this.labelDataGridViewCount = new System.Windows.Forms.Label();
this.labelSpinsBetweenInsertsText = new System.Windows.Forms.Label();
this.numericUpDownTimeoutBetweenInserts = new System.Windows.Forms.NumericUpDown();
this.checkBoxToggleWorker = new System.Windows.Forms.CheckBox();
this.timerGuiUpdater = new System.Windows.Forms.Timer(this.components);
((System.ComponentModel.ISupportInitialize)(this.dataGridView)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.personBindingSource)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.numericUpDownTimeoutBetweenInserts)).BeginInit();
this.SuspendLayout();
//
// dataGridView
//
this.dataGridView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.dataGridView.AutoGenerateColumns = false;
this.dataGridView.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill;
this.dataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.Index,
this.lastNameDataGridViewTextBoxColumn,
this.firstNameDataGridViewTextBoxColumn,
this.bornDataGridViewTextBoxColumn,
this.ageDataGridViewTextBoxColumn});
this.dataGridView.DataSource = this.personBindingSource;
this.dataGridView.Location = new System.Drawing.Point(12, 12);
this.dataGridView.Name = "dataGridView";
this.dataGridView.Size = new System.Drawing.Size(560, 212);
this.dataGridView.TabIndex = 0;
//
// Index
//
this.Index.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.DisplayedCells;
this.Index.DataPropertyName = "Index";
this.Index.HeaderText = "Index";
this.Index.Name = "Index";
this.Index.Width = 58;
//
// lastNameDataGridViewTextBoxColumn
//
this.lastNameDataGridViewTextBoxColumn.DataPropertyName = "LastName";
this.lastNameDataGridViewTextBoxColumn.HeaderText = "LastName";
this.lastNameDataGridViewTextBoxColumn.Name = "lastNameDataGridViewTextBoxColumn";
//
// firstNameDataGridViewTextBoxColumn
//
this.firstNameDataGridViewTextBoxColumn.DataPropertyName = "FirstName";
this.firstNameDataGridViewTextBoxColumn.HeaderText = "FirstName";
this.firstNameDataGridViewTextBoxColumn.Name = "firstNameDataGridViewTextBoxColumn";
//
// bornDataGridViewTextBoxColumn
//
this.bornDataGridViewTextBoxColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.DisplayedCells;
this.bornDataGridViewTextBoxColumn.DataPropertyName = "Born";
this.bornDataGridViewTextBoxColumn.HeaderText = "Born";
this.bornDataGridViewTextBoxColumn.Name = "bornDataGridViewTextBoxColumn";
this.bornDataGridViewTextBoxColumn.Width = 54;
//
// ageDataGridViewTextBoxColumn
//
this.ageDataGridViewTextBoxColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.DisplayedCells;
this.ageDataGridViewTextBoxColumn.DataPropertyName = "Age";
this.ageDataGridViewTextBoxColumn.HeaderText = "Age";
this.ageDataGridViewTextBoxColumn.Name = "ageDataGridViewTextBoxColumn";
this.ageDataGridViewTextBoxColumn.ReadOnly = true;
this.ageDataGridViewTextBoxColumn.Width = 51;
//
// personBindingSource
//
this.personBindingSource.DataSource = typeof(WindowsFormsApplication.Person);
//
// backgroundWorker
//
this.backgroundWorker.WorkerSupportsCancellation = true;
this.backgroundWorker.DoWork += new System.ComponentModel.DoWorkEventHandler(this.OnBackgroundWorkerDoWork);
this.backgroundWorker.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(this.OnBackgroundWorkerRunWorkerCompleted);
//
// labelDataGridViewCountText
//
this.labelDataGridViewCountText.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.labelDataGridViewCountText.Location = new System.Drawing.Point(12, 230);
this.labelDataGridViewCountText.Name = "labelDataGridViewCountText";
this.labelDataGridViewCountText.Size = new System.Drawing.Size(50, 23);
this.labelDataGridViewCountText.TabIndex = 1;
this.labelDataGridViewCountText.Text = "Count:";
this.labelDataGridViewCountText.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// labelDataGridViewCount
//
this.labelDataGridViewCount.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.labelDataGridViewCount.Location = new System.Drawing.Point(68, 230);
this.labelDataGridViewCount.Name = "labelDataGridViewCount";
this.labelDataGridViewCount.Size = new System.Drawing.Size(82, 23);
this.labelDataGridViewCount.TabIndex = 2;
this.labelDataGridViewCount.Text = "0";
this.labelDataGridViewCount.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// labelSpinsBetweenInsertsText
//
this.labelSpinsBetweenInsertsText.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.labelSpinsBetweenInsertsText.Location = new System.Drawing.Point(265, 230);
this.labelSpinsBetweenInsertsText.Name = "labelSpinsBetweenInsertsText";
this.labelSpinsBetweenInsertsText.Size = new System.Drawing.Size(155, 23);
this.labelSpinsBetweenInsertsText.TabIndex = 3;
this.labelSpinsBetweenInsertsText.Text = "Spins between inserts:";
this.labelSpinsBetweenInsertsText.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// numericUpDownTimeoutBetweenInserts
//
this.numericUpDownTimeoutBetweenInserts.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.numericUpDownTimeoutBetweenInserts.Increment = new decimal(new int[] {
10,
0,
0,
0});
this.numericUpDownTimeoutBetweenInserts.Location = new System.Drawing.Point(426, 233);
this.numericUpDownTimeoutBetweenInserts.Maximum = new decimal(new int[] {
500,
0,
0,
0});
this.numericUpDownTimeoutBetweenInserts.Minimum = new decimal(new int[] {
10,
0,
0,
0});
this.numericUpDownTimeoutBetweenInserts.Name = "numericUpDownTimeoutBetweenInserts";
this.numericUpDownTimeoutBetweenInserts.Size = new System.Drawing.Size(65, 20);
this.numericUpDownTimeoutBetweenInserts.TabIndex = 4;
this.numericUpDownTimeoutBetweenInserts.ThousandsSeparator = true;
this.numericUpDownTimeoutBetweenInserts.Value = new decimal(new int[] {
500,
0,
0,
0});
this.numericUpDownTimeoutBetweenInserts.ValueChanged += new System.EventHandler(this.OnNumericUpDownTimeoutBetweenInsertsValueChanged);
//
// checkBoxToggleWorker
//
this.checkBoxToggleWorker.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.checkBoxToggleWorker.Appearance = System.Windows.Forms.Appearance.Button;
this.checkBoxToggleWorker.Location = new System.Drawing.Point(497, 230);
this.checkBoxToggleWorker.Name = "checkBoxToggleWorker";
this.checkBoxToggleWorker.Size = new System.Drawing.Size(75, 23);
this.checkBoxToggleWorker.TabIndex = 6;
this.checkBoxToggleWorker.Text = "&Start";
this.checkBoxToggleWorker.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
this.checkBoxToggleWorker.UseVisualStyleBackColor = true;
this.checkBoxToggleWorker.CheckedChanged += new System.EventHandler(this.OnCheckBoxToggleWorkerCheckedChanged);
//
// timerGuiUpdater
//
this.timerGuiUpdater.Tick += new System.EventHandler(this.OnTimerGuiUpdaterTick);
//
// FormMain
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(584, 262);
this.Controls.Add(this.checkBoxToggleWorker);
this.Controls.Add(this.numericUpDownTimeoutBetweenInserts);
this.Controls.Add(this.labelSpinsBetweenInsertsText);
this.Controls.Add(this.labelDataGridViewCount);
this.Controls.Add(this.labelDataGridViewCountText);
this.Controls.Add(this.dataGridView);
this.MinimumSize = new System.Drawing.Size(600, 300);
this.Name = "FormMain";
this.Text = "DataGridView Performance Tester";
((System.ComponentModel.ISupportInitialize)(this.dataGridView)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.personBindingSource)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.numericUpDownTimeoutBetweenInserts)).EndInit();
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.DataGridView dataGridView;
private System.ComponentModel.BackgroundWorker backgroundWorker;
private System.Windows.Forms.BindingSource personBindingSource;
private System.Windows.Forms.Label labelDataGridViewCountText;
private System.Windows.Forms.Label labelDataGridViewCount;
private System.Windows.Forms.Label labelSpinsBetweenInsertsText;
private System.Windows.Forms.NumericUpDown numericUpDownTimeoutBetweenInserts;
private System.Windows.Forms.CheckBox checkBoxToggleWorker;
private System.Windows.Forms.Timer timerGuiUpdater;
private System.Windows.Forms.DataGridViewTextBoxColumn Index;
private System.Windows.Forms.DataGridViewTextBoxColumn lastNameDataGridViewTextBoxColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn firstNameDataGridViewTextBoxColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn bornDataGridViewTextBoxColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn ageDataGridViewTextBoxColumn;
}
Last but not least my little person class that is used for the payload:
public class Person
{
public int Age
{
get
{
// ToDo: better algorithm to determine real age is left as an exercise to the reader. ;-)
var age = (int)((DateTime.Now - Born).TotalDays / 365);
return Math.Max(0, age);
}
}
public DateTime Born { get; set; }
public string FirstName { get; set; }
public int Index { get; set; }
public string LastName { get; set; }
}
Is the bottleneck in processing data from server or in actually putting it to DataGridView? If latter, VirtualMode can help you: http://msdn.microsoft.com/en-us/library/2b177d6d.aspx.
Are you using a BackgroundWorker? Put the code that makes you application to freeze in DoWork event:
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
YourFreezingCodeHere
}
And start backgroundWorker like
backgroundWorker1.RunWorkerAsync();
You can do this using BackgroundWorker. In the DoWork method you can iterate update the datagrid.
To update datagrid from Non-UI thread you will need to as follows
Create an extension method like
public static class ControlExtensions
{
public static void Invoke(this Control control, Action action)
{
if (control.InvokeRequired) control.Invoke(new MethodInvoker(action), null);
else action.Invoke();
}
}
Update data grid as (assuming dataGrid is your control id and dataSource is your data source)
dataGrid.Invoke(() => { dataGrid.DataSource = dataSource; };
Hope this works for you.
The UI will always be updated by the main/UI thread. That is the way WinForms works.
What you can do is prevent the UI-thread from doing too much. To do that:
Make sure you execute all other processing on one or more different threads.
Only update the UI when useful for the user. I can't see/read a number that is changing every 3 ms so skip DISPLAYING the update.
Note that I use the terms ViewModel, View and Model is the remainder of this answer. I am not forcing you to use MVVM but it makes explaining things easier. You could use MVP or MVC in the same way.
You could create a special kind of ViewModel that raises an event after x milliseconds to check for a 'dirty bits' and raise appropriate PropertyChanged events. This would require you to set the dirty bits in the property setters and NOT raise the PropertyChanged event in the setters.
Perhaps even better might be to keep track of the last time a ViewModel was updated; when it is longer than x milliseconds ago, update the ViewModel from the Model, otherwise don't. This guarantees the UI to be in-sync with the ViewModel. But you have to realize that the ViewModel is not in-sync with the Model. Of course it is possible to create methods to sync the models explicitly.
The choice between these two might depend on how you think about the View-ViewModel relation and how much time this all is costing.
Application.DoEvents();
Use this method inside timer.
private void timer1_Tick(object sender, EventArgs e)
{
Application.DoEvents();
}
You should start timer where your UI freezes or you can start it in form_Load and set small number to your timer's interval to make it ticks frequently. For Example set it to ten.
timer1.Start();
timer1.Interval = 10;
I've done a lot of high volume data transfers (hundreds per second) like this and I think a DataGrid just isn't the control you want. It's designed to present data and let the user edit it, it's not really optimized to be an information stream. At this volume it won't do a user much good to view the data in real time, it's just going to be a stream of data too large and fast to make sense of.
I suggest you continue to use a background worker to do the work (like you said you are) and use the ReportProgress method to send a % done back to a progress bar. You can also update a label on the page with the file you're working on. The label will update automatically and not freeze your UI. To do this, create an instance variable in the class your background worker calls. On your UI, create an instance of that class and in the background worker's ProgressChanged method set your UI label to your class instance variable. It will update each time you call backgroundworker.ReportProgress()
Then put all the info in a log so someone can look at it later. It's just not that beneficial to try to visually take in 350 changes/second.
One solution to this problem is to update your data model periodically, i.e. to batch update them from the communication thread every x milliseconds. A little more information on how you are accessing the server data would be helpful in giving you a more explicit suggestion.
At the same time you should be using virtualised controls (especially the datagrid). Using a virtual grid basically means visible cells are rendered on the fly. Hence you only need to update the data that is currently displayed. As each cell becomes visible it will access the data model to obtain the relevent value at that time. See this link as a starting point to implementing a virtual grid.
By combining these two approaches you should be able to minimise the amount of updates to the grid.
re: The problem is that the UI hangs when such an update happens because it has to happen on the main thread. Is there a known solution for this problem?
no, as you're seeing
Generally, how can you design highly performant enterprise applications in WinForms where the UI is being updated continuously without the application freezing?
At the scale you're describing, you can't. Try limiting the scope of your UI control so it doesn't try to display everything happening everywhere at once, but forces the user to pick an area to focus on that you can update at an acceptable rate.
I recommend to use two layers for handling this scenario.
Non-UI Data Layer:
This layer can grab all updates from your background thread and generate final Data object (Let's call it ObjectX) which is the latest and most current state of data. This layer should run on it's own thread which will not effect UI at all. Also after receiving any update you can have one boolean variable (Let's call it NewUpdateExist) and set it to true which indicates that new changes has been received. Don't forget to use Thread-safe locking mechanism to set this variable to True, to avoid concurrency issues.
UI Sync Layer:
This layer also can run on separated thread. You can have a timer which will trigger on specific interval(*) to checks if there is any new data since last UI update by checking out NewUpdateExist variable and if there is, then Set NewUpdateExist to false and generate new sub-set of data which only required to display on Screen (***). Don't forget to use Thread-safe locking while generating sub-set of data and updating NewUpdateExist variable.
After generating your sub-set of data then you need to Invoke another method by control (In UI Thread) to apply this sub-set of data to the control. This is where UI thread will block until process done so you need to make this method as light as possible! All heavy stuff needs to be done outside of Invoke and only part that relate to UI control needs to be in that method.
(*) As "Hans Passant" mentioned in his comment, human eye only can process 50 milliseconds refresh, but I even suggest to increase this to 100 msec. You can get some ideas from this thread:
What is the shortest perceivable application response delay?
(**) Tricky part in this case is how to update your control with only data that is required instead of pushing all data at once to the UI. I really recommend to implement custom controls to handle this part instead of using standard controls; because you'll have full access to how and when to update UI and you can achieve best performance. For example on Grid you can find out first visible item and number of items that can be displayed on UI and just update that portion instead of trying to update control with all data.
Sorry, I know I suppose to explain solution in short message but this is the shortest version that I can came across. I hope this helps :-)
There is an article posted on MSDN regarding Asynchronous Call Pattern for Windows Forms. I hope this helps.
Use backgroundWorker, it will run inserted code in separated thread, so the application will not freeze.
public void backgroundWorkerPinger_DoWork(object sender, DoWorkEventArgs e)
{
Ping ping = new Ping();
try
{
PingReply pingreply = ping.Send("46.4.106.10", 500);
string active = pingreply.Status.ToString();
if (active == "Success")
{
//Pokud je spojení aktivni pak se nastavi barva labelu na zelenou a vypise se aktivni
ActiveOrNotLabel.ForeColor = Color.Green;
ActiveOrNotLabel.Text = "Aktivní";
// MessageBox.Show("vyjimka2");
if (connection_enabled == false)
{
admini.Enabled = true;
connection_enabled = true;
}
}
if (active != "Success") {
ActiveOrNotLabel.ForeColor = Color.Red;
ActiveOrNotLabel.Text = "Neaktivní";
admini.Enabled = false;
connection_enabled = false;
}
}
catch
{
//Jinak na cervenou a neaktivni
//MessageBox.Show("vyjimka");
ActiveOrNotLabel.ForeColor = Color.Red;
ActiveOrNotLabel.Text = "Neaktivní";
admini.Enabled = false;
connection_enabled = false;
}
}

Build time based bars using stock tick data

I'm trying to build stock market Bar (snapshot) data at run-time using tick data. My stock data provider provides access to tick level data where I have an event called OnTick that is triggered whenever a new tick is sent by the data provider. I'm hoping to do one of the two below, or if someone can suggest a good option:
Option 1:
In this option I maintain a Bar object and update it each time I get a tick. The OnBar() event can be attached to a timer elapsed event (1 minute for 1 minute bars etc).
//TickMsg = double price, DateTime dttm
public void OnTick(TickMsg newTick)
{
TaskFactory.StartNew(){UpdateBar(newTick)};//Syntax not specific
}
UpdateBar()
{
//nextBar is a Bar object thats intialized to Open = 0, High = 0, Low = 0, Close = 0
if(nextBar.Open==0)
nextBar.Open = newTick.price;
if(newTick.price>nextBar.High)
nextBar.High = newTick.price;
if(newTick.price<nextBar.Low)
nextBar.Low = newTick.price;
nextBar.Close = newTick.price;
}
public void OnBar(Bar bar)
{
//Process the bar..perform calculations etc
bar = new Bar(0,0,0,0);//Reset the bar
}
Option 2:
In this option I'm just adding the tick to a list of ticks and perform the calculations when OnBar is called. The OnBar() event can be attached to a timer elapsed event (1 minute for 1 minute bars etc).
List <TickMsg> TickList;
public void OnTick(TickMsg newTick)
{
TickList.Add(newTick);
}
public void OnBar()//called on a timer
{
var low = TickList.Min();
var high = TickList.Max();
var close = (from entry in TickList orderby entry.TickMsg.dttm ascending).Last();
var open = (from entry in TickList orderby entry.TickMsg.dttm ascending).First();
TickList.Empty();
}
Questions:
Which approach is more processing intensive?
Which approach requires more memory?
Again, if someone has a suggestion on an alternative approach, I'm all ears.
Don't you need to display or access the bar before it's completed? In case option 2 seems not to achieve that. Option 1 I can never imagine will be a performance clog. And it will use less memory, as you don't seem to save the tick data to any variable.
I think the best approach it is the second.
When you reset the bar on first approach, the lowest price will never be less than zero, so, the low price on bar will be always zero.
Trade link is a good example on how it is done. Their tutorials cover this. Also since it is open source you can have a peek into how it is done. The getting started tutorial is here.

Monotouch ipad memory / animation problem

All, I'm working on what I thought was a fairly simple app. I'm using multiple view controllers with a view - under which there are buttons and a single image view. the buttonpress event triggers the other viewcontroller's view to display. That works perfectly. However, I'm also wanting to animate the transition to simulate a page turn. I use the code below to do that. It works well, however, every time I use this method the memory used increases. The memory used appears to be disconnected from the actual size of the image array. Also, I changed from png to jpeg ( much smaller images ) and it doesn't make a bit of difference. I thought about using .mov but the load time is very noticeable.
Please help. I've tried a ton of different way to force garbage collection. I've dug through the limited texts, and searched this website to no avail.
Here's a sample of the code.
public partial class AppDelegate : UIApplicationDelegate
{
// This method is invoked when the application has loaded its UI and its ready to run
public override bool FinishedLaunching (UIApplication app, NSDictionary options)
{
UIApplication.SharedApplication.SetStatusBarHidden( true, true);
// If you have defined a view, add it here:
// window.AddSubview (navigationController.View);
//window.AddSubview(mainController.View);
window.MakeKeyAndVisible ();
coverOpenbtn.TouchUpInside += HandleCoverOpenbtnTouchUpInside;
backBtn1.TouchUpInside += HandleBackBtn1TouchUpInside;
return true;
}
void HandleBackBtn1TouchUpInside (object sender, EventArgs e)
{
this.navView.RemoveFromSuperview();
List<UIImage> myImages = new List<UIImage>();
myImages.Add(UIImage.FromFile("c_1_00011.jpg"));
myImages.Add(UIImage.FromFile("c_1_00010.jpg"));
myImages.Add(UIImage.FromFile("c_1_00009.jpg"));
myImages.Add(UIImage.FromFile("c_1_00008.jpg"));
myImages.Add(UIImage.FromFile("c_1_00007.jpg"));
myImages.Add(UIImage.FromFile("c_1_00006.jpg"));
myImages.Add(UIImage.FromFile("c_1_00005.jpg"));
myImages.Add(UIImage.FromFile("c_1_00004.jpg"));
myImages.Add(UIImage.FromFile("c_1_00003.jpg"));
myImages.Add(UIImage.FromFile("c_1_00002.jpg"));
myImages.Add(UIImage.FromFile("c_1_00001.jpg"));
myImages.Add(UIImage.FromFile("c_1_00000.jpg"));
//myImages.Add(UIImage.FromFile("c_1_00012.jpg"));
var myAnimatedView = new UIImageView(window.Bounds);
myAnimatedView.AnimationImages = myImages.ToArray();
myAnimatedView.AnimationDuration = 1; // Seconds
myAnimatedView.AnimationRepeatCount = 1;
myAnimatedView.StartAnimating();
window.AddSubview(myAnimatedView);
}
void HandleCoverOpenbtnTouchUpInside (object sender, EventArgs e)
{
this.coverView.AddSubview(navView);
List<UIImage> myImages = new List<UIImage>();
myImages.Add(UIImage.FromFile("c_1_00000.jpg"));
myImages.Add(UIImage.FromFile("c_1_00001.jpg"));
myImages.Add(UIImage.FromFile("c_1_00002.jpg"));
myImages.Add(UIImage.FromFile("c_1_00003.jpg"));
myImages.Add(UIImage.FromFile("c_1_00004.jpg"));
myImages.Add(UIImage.FromFile("c_1_00005.jpg"));
myImages.Add(UIImage.FromFile("c_1_00006.jpg"));
myImages.Add(UIImage.FromFile("c_1_00007.jpg"));
myImages.Add(UIImage.FromFile("c_1_00008.jpg"));
myImages.Add(UIImage.FromFile("c_1_00009.jpg"));
myImages.Add(UIImage.FromFile("c_1_00010.jpg"));
myImages.Add(UIImage.FromFile("c_1_00011.jpg"));
//myImages.Add(UIImage.FromFile("c_1_00012.jpg"));
var myAnimatedView = new UIImageView(window.Bounds);
myAnimatedView.AnimationImages = myImages.ToArray();
myAnimatedView.AnimationDuration = 1; // Seconds
myAnimatedView.AnimationRepeatCount = 1;
opened++;
}
myAnimatedView.StartAnimating();
window.AddSubview(myAnimatedView);
}
Here's a few hints (just by reading the code):
There no difference between JPEG and PNG once the images are loaded in memory. The format only matters when the image is stored, not displayed. Once loaded (and decompressed) they will take a bit over (Width * Height * BitCount) of memory.
Consider caching your images and load them only they are not available. The GC will decide when to collect them (so many copies could exists at the same time). Right now you're loading each image twice when you could do it once (and use separate array for ordering them).
Even if you cache them also be ready to clear them on demand, e.g. if iOS warns you memory is low. Override ReceiveMemoryWarning to clear your list (or better arrays).
Don't call ToArray if you can avoid it (like your sample code). If you know how many images you have them simply create the array with the right size (and cache both array too ;-). It will cut down (a bit) the allocations;
Even consider caching the 'myAnimatedView' UIImageView (if the above did not help enough)
Be helpful to others, try them one-by-one and tell us what help you the most :-)
The images are to "animate" a page turn...is this to navigate through the app?
E.g. you start at the "home" page, press a button then it animates a page turn to the next screen in your app?
I think you would be better off looking at using CoreGraphics to try and achieve this effect, it'll both be a lot more efficient memory wise, and it will probably look a lot better as well. There are a few projects in Objective-C to get you started, such as Tom Brow's excellent Leaves project.
Okay, here is the best solution I found, doesn't crash the hardware, and is generally useful for other tasks.
This is the code that goes in the handler for the button press, each NavImage is a UIImage I built under the same view in interface builder. I just turned the alpha to 0 initially, and light them up one by one...
NSTimer.CreateScheduledTimer(.1,delegate { navImage1.Alpha = 1; NSTimer.CreateScheduledTimer(.1,delegate { navImage2.Alpha = 1;
NSTimer.CreateScheduledTimer(.05,delegate { navImage3.Alpha = 1;
NSTimer.CreateScheduledTimer(.05,delegate { navImage4.Alpha = 1;
NSTimer.CreateScheduledTimer(.05,delegate { navImage5.Alpha = 1;
NSTimer.CreateScheduledTimer(.05,delegate { navImage6.Alpha = 1;
NSTimer.CreateScheduledTimer(.05,delegate { navImage7.Alpha = 1;
NSTimer.CreateScheduledTimer(.05,delegate { navImage8.Alpha = 1;
NSTimer.CreateScheduledTimer(.05,delegate { navImage9.Alpha = 1;
NSTimer.CreateScheduledTimer(.05,delegate { navImage.Alpha = 1; });});});});});});});});});});

Categories

Resources