Caching images in another thread before user sees it - c#

I'm working on a eBook program in WPF recently, and the pages of the book are all saved as .jpg format. Since the images are all in high quality, the program will lag while switching pages.
I tried to cache a few images after the current image which the user is seeing in another thread. But it gives me an error.
An unhandled exception of type 'System.InvalidOperationException' occurred in WindowsBase.dll
Additional information: The calling thread cannot access this object because a different thread owns it.
I can fix the error by adding Invoke, but the performance is like without the memory cache.
But without Invoke, the programs shows the error above. And I don't know how to fix it.
I tried to make the code as simple as I could.
Any help is appreciated.
Full Code:
using System;
using System.Collections.Specialized;
using System.IO;
using System.Runtime.Caching;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media.Imaging;
namespace ThreadedMemoryCache_Test
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
MemoryCache memcache;
int num = 0;
Image img;
int BufferPages = 2;
public MainWindow()
{
InitializeComponent();
//Change Content
img = new Image();
Content = img;
//Mouse Events
MouseDown += (s, e) =>
{
int add = 0;
if (e.LeftButton == MouseButtonState.Pressed)
add--;
else if (e.RightButton == MouseButtonState.Pressed)
add++;
else
return;
num += add;
img.Source = GetBitmap(num); //Error here
Buffer();
};
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
//Initiallize MemoryCache
NameValueCollection CacheSettings = new NameValueCollection(3);
CacheSettings.Add("CacheMemoryLimitMegabytes", Convert.ToString(1));
CacheSettings.Add("physicalMemoryLimitPercentage", Convert.ToString(10));
CacheSettings.Add("pollingInterval", Convert.ToString("00:00:10"));
memcache = new MemoryCache("BitmapCache", CacheSettings);
//Get First Photo
img.Source = GetBitmap(num);
Buffer();
}
BitmapImage GetBitmap(int num)
{
String id = num.ToString();
BitmapImage cachebmp = memcache[id] as BitmapImage;
if (cachebmp == null)
{
CacheItemPolicy policy = new CacheItemPolicy();
String name = num.ToString() + ".jpg";
FileInfo file = new FileInfo(name);
if(!file.Exists)
{
num = 0;
return GetBitmap(num);
}
cachebmp = new BitmapImage(new Uri(file.FullName));
memcache.Set(id, cachebmp, policy);
}
return cachebmp;
}
void Buffer()
{
//Start Thread
Thread cachethread = new Thread(() =>
{
for (int i = 1; i <= BufferPages; i++)
{
//Adding invoke fixes the error, but it seems to delay the main thread and cause lag.
//this.Dispatcher.Invoke((Action)(() =>
//{
GetBitmap(num + i);
//}));
}
});
cachethread.IsBackground = true;
cachethread.Start();
}
}
}

You have to freeze the BitmapImage to make it cross-thread accessible:
cachebmp = new BitmapImage(new Uri(file.FullName));
cachebmp.Freeze();
memcache.Set(id, cachebmp, policy);
That said, your code could be improved in many ways. Better use a ThreadPool thread or a BackgroundWorker or a Task to perform the background task.
Besides that you shouldn't call GetBitmap(num) recursively with num = 0 in case an image file doesn't exist. What happens if the 0.jpg is also missing?

You should use BackgroundWorker class.
Load some initial images at startup in MainThread, then start BackgroundWorker for rest of images.
Useful links : How to: Use a Background Worker, BackgroundWorker In Wpf.

Related

Why would my timer does not dynamically display the file size?

So basically, I m making a program where 25 tasks write the same random task to one file until it reaches a certain size, and I would like to dynamically display the file size of the selected file in 0.5 seconds interval, so I made a timer hooked with Timer_Elapsed which should be executed every 0.5 seconds and display on UI which I specify on mainWindow.xaml on the textblock x:Name="fileSize", so I placed the createTimer function in to btnGo_Click function in mainwindow.xaml.cs so the event would extract the right fileInfo of the selectedFile. Any advice for my wrong would be appreciated. I'm also sharing the FileIO class in case it is needed, so they are full solutions. Even aside from the questions I asked, any general advice to better my code would be appreciated because I need to get a grasp of the good code example.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Windows;
using System.Threading;
namespace WriteFile
{
internal class FileIO
{
private object _lock = new object();
public volatile bool sizeReached = false;
private StreamWriter sw = null;
Mutex mut = null;
public FileIO()
{
if (!Mutex.TryOpenExisting("MyMutex", out mut))
{
mut = new Mutex(true, "MyMutex");
mut.ReleaseMutex();
}
}
internal void WriteFile(string FilePath)
{
while (!sizeReached)
{
mut.WaitOne();
try
{
using (sw = new StreamWriter(FilePath, true))
{
sw.WriteLine(Guid.NewGuid());
}
}
catch (Exception ex)
{
MessageBox.Show("Exception: " + ex.Message);
}
finally
{
if (sw != null)
{
sw.Close();
}
}
mut.ReleaseMutex();
}
}
internal void SizeMonitor(string FPath, int MaxSize, Task[] tasks)
{
FileInfo fi = null;
while (!sizeReached)
{
if (File.Exists(FPath))
{
fi = new FileInfo(FPath);
if (fi.Length >= MaxSize)
{
sizeReached = true;
}
}
if (sizeReached)
{
foreach (Task task in tasks)
{
task.Wait();
}
}
Thread.Sleep(1);
}
MessageBox.Show(fi.Length.ToString());
MessageBox.Show("Done");
}
}
}
mainWindow.xaml
<Window x:Class="WriteFile.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WriteFile"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<TextBlock x:Name="fileSize"/>
<TextBox Name ="TargetSize" VerticalAlignment="Center" FontSize="20">
</TextBox>
<Label Content="Target Size" HorizontalAlignment="Left" Margin="92,150,0,0" VerticalAlignment="Top"/>
<Button Name ="btnGo" Content="Write to file" HorizontalAlignment="Left" Margin="92,267,0,0" VerticalAlignment="Top" Width="100" Click="btnGo_Click"/>
</Grid>
</Window>
mainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.IO;
using Microsoft.Win32;
using System.ComponentModel;
using System.Timers;
using System.Threading;
using System.Runtime.CompilerServices;
namespace WriteFile
{
public partial class MainWindow : Window
{
System.Timers.Timer timer = new System.Timers.Timer();
Task[] tasks;
Task MonitorTask;
static FileIO fio = new FileIO();
static string fPath;
static FileInfo fileInfo;
public MainWindow()
{
InitializeComponent();
CreateTimer();
}
public void CreateTimer()
{
var timer = new System.Timers.Timer(500); // fire every 0.5 second
timer.Enabled = true;
timer.Elapsed += Timer_Elapsed;
}
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
fileSize.Text = fileInfo.Length.ToString();
}
private void btnGo_Click(object sender, RoutedEventArgs e)
{
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.ShowDialog();
Stream myStream;
saveFileDialog.FilterIndex = 2;
saveFileDialog.RestoreDirectory = true;
if (File.Exists(saveFileDialog.FileName))
{
File.Delete(saveFileDialog.FileName);
}
if ((myStream = saveFileDialog.OpenFile()) != null)
{
StreamWriter sw = new StreamWriter(myStream);
sw.Write(" your text");
myStream.Close();
}
int NoOfTasks = 25;
int Target = Convert.ToInt32(TargetSize.Text);
fPath = saveFileDialog.FileName;
tasks = new Task[NoOfTasks];
fio.sizeReached = false;
fileInfo = new FileInfo(fPath);
for (int i = 0; i < NoOfTasks; i++)
{
tasks[i] = new Task(() => fio.WriteFile(fPath));
tasks[i].Start();
}
MonitorTask = new Task(() => fio.SizeMonitor(fPath, Target, tasks));
MonitorTask.Start();
}
}
}
As mentioned by others, FileInfo.Length value is pre-cached. If the associated file has changed, the FileInfo requires to refresh those cached values. To accomplish this, you must explicitly call the FileInfo.Refresh method. FileInfo does not monitor the associated file.
Furthermore, System.Threading.Timer executes the callback on a background thread. In WPF you can only reference a DispatcherObject (for example TextBlock) from a dispatcher thread (UI thread). For this reason you should use the DispatcherTimer. DispatcherTimer executes the callback on the dispatcher thread.
Additionally, you would want to stop the timer once the writing to the file is completed.
You should not use Task.Start. Instead of Task.Start and a Mutex you should simply await the Task. In your context, you can await a collection of Task objects using Task.WhenALl.
And instead of creating Task explicitly, you should use the async API of the StreamWriter.
You actually don't have concurrent code (neither would you benefit from it): the Mutex, declaration of a volatile field and the lock object are redundant. Also closing the StreamWriter explicitly in a finally block is not needed as you already use a using-block to handle the lifetime of the instance.
There are some more flaws in your code (some logical issues for instance). I have refactored your version to eliminate some of the issues for example by using the asynchronous StreamWriter API and async/await.
Because it is difficult to make any sense of your code (due to the lack of context) it is not possible to further improve it. For example, creating a single concatenated string (consisting of e.g. 25 values) would result in a single file write (and resource allocation), which would significantly improve the overall performance.
FileIO.cs
namespace WriteFile
{
internal class FileIO
{
public bool IsSizeReached { get; private set; }
public FileIO()
{
}
internal async Task WriteToFileAsync(string filePath)
{
if (this.IsSizeReached)
{
return;
}
using (var sw = new StreamWriter(filePath, true))
{
await sw.WriteLineAsync(Guid.NewGuid());
}
}
internal async Task SizeMonitorAsync(string fPath, int maxSize, IEnumerable<Task> tasks)
{
FileInfo fi = new FileInfo(fPath);
this.IsSizeReached = fi.Length >= maxSize;
if (this.IsSizeReached)
{
return;
}
await Task.WhenAll(tasks);
MessageBox.Show(fi.Length.ToString());
MessageBox.Show("Done");
}
}
}
MainWindow.xaml.cs
namespace WriteFile
{
public partial class MainWindow : Window
{
private DispatcherTimer Timer { get; }
private FileIO Fio { get; }
private FileInfo DestinationFileInfo { get; }
public MainWindow()
{
InitializeComponent();
this.Fio = new FileIO();
}
public void StartTimer()
{
this.Timer = new DispatcherTimer(
TimeSpan.FromMilliseconds(500),
DispatcherPriority.Background,
OnTimerElapsed,
this.Dispatcher);
this.Timer.Start();
}
public void StopTimer() => this.Timer.Stop();
private void OnTimerElapsed(object sender, EventArgs e)
{
this.fileSize.Text = this.DestinationFileInfo.Length.ToString();
}
private async void btnGo_Click(object sender, RoutedEventArgs e)
{
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.ShowDialog();
saveFileDialog.FilterIndex = 2;
saveFileDialog.RestoreDirectory = true;
if (File.Exists(saveFileDialog.FileName))
{
File.Delete(saveFileDialog.FileName);
}
Stream myStream;
if ((myStream = saveFileDialog.OpenFile()) != null)
{
// Dispose StreamWriter and underlying Stream
using (var sw = new StreamWriter(myStream))
{
await sw.WriteAsync(" your text");
}
}
int noOfTasks = 25;
// TODO::You must validate the input to prevent invalid or unreasonable values.
// Currently, this line will throw and crash the application if the input is invalid (not numeric).
int target = Convert.ToInt32(TargetSize.Text);
string fPath = saveFileDialog.FileName;
var tasks = new List<Task>();
this.DestinationFileInfo = new FileInfo(fPath);
StartTimer();
for (int i = 0; i < noOfTasks; i++)
{
Task writeToFileTask = this.Fio.WriteToFileAsync(fPath);
tasks.Add(writeToFileTask);
}
await this.Fio.SizeMonitorAsync(fPath, target, tasks));
StopTimer();
}
}
}
Timers.Timer works asynchronously and it raises the Elapsed event on the pool thread if SynchronizingObject is not set.
And work with WPF UI elements can only be done on the thread of their Dispatcher (almost always this is the main UI thread of the application).
Two solutions out of many possible:
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
// Asynchronously getting the right data
string text = fileInfo.Length.ToString();
// Using the Window's Dispatcher
// to setting ready-made data to UI elements.
Dispatcher.BeginInvoke(() => fileSize.Text = text);
}
The second option is without using a timer:
public MainWindow()
{
Initialized += RefreshAsync;
InitializeComponent();
}
public async void RefreshAsync(object? sender, EventArgs e)
{
while(true)
{
string text = await Task.Run(() => fileInfo.Length.ToString());
fileSize.Text = text;
await Task.Delay(500);
}
}

Play Gif in WPF

I am currently trying to display and play an animated gif in WPF without much luck
I have tried all the solutions found here including the one in the question
Here is what i currently have
<Window x:Class="Sooooothing.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Attached="clr-namespace:Sooooothing"
Title="MainWindow" Height="327.861" Width="525">
<Grid>
<MediaElement x:Name="gif" MediaEnded="myGif_MediaEnded" UnloadedBehavior="Manual"
Source="Images\7c6516d73fc8643abf1df969fcc1a72c.gif"
LoadedBehavior="Play" Stretch="None"/>
</Grid>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Sooooothing
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
//gif.Play();
}
private void myGif_MediaEnded(object sender, RoutedEventArgs e)
{
gif.Position = new TimeSpan(0, 0, 1);
gif.Play();
}
}
}
When the application starts all i get is the white background for the window, at first i though it was the gif not loading but i let it sit for a couple minutes without any change.
Here is my solution
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Sooooothing
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
GifBitmapDecoder decoder2;
int ImageNumber = 0;
bool mouseIn = true;
double opasity = 0;
List<Thread> threadList = new List<Thread>();
bool programClosed = false;
public MainWindow()
{
InitializeComponent();
Classes.DataHouse.load(); //Im storing all my gif links in this List<string> so i can
// loop through them with buttons on the window
#region thread gif frame changer
Uri myUri = new Uri(Classes.DataHouse.images[ImageNumber], UriKind.RelativeOrAbsolute);
decoder2 = new GifBitmapDecoder(myUri, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
BitmapSource bitmapSource2; // Decode the gif using GifBitmapDecoder then start a thread to
int frameCount = decoder2.Frames.Count;// loop through all the images and put them one after
Thread th = new Thread(() => {// another into an image on your window
while (!programClosed)
{
for (int i = 0; i < frameCount; i++)
{
try{
this.Dispatcher.Invoke(new Action(delegate()
{
frameCount = decoder2.Frames.Count;
if (frameCount >= i)
{
bitmapSource2 = decoder2.Frames[i];
image.Source = bitmapSource2;
}
}));
}
catch
{
//the thread was probably aborted
}
System.Threading.Thread.Sleep(100);
}
}
});
threadList.Add(th);
th.Start();
#endregion
}
private void Window_MouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left)
this.DragMove();
}
private void img_forward_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (ImageNumber+1 >= Classes.DataHouse.images.Count)
ImageNumber = 0;
else
ImageNumber++;
Uri myUri = new Uri(Classes.DataHouse.images[ImageNumber], UriKind.RelativeOrAbsolute);
decoder2 = new GifBitmapDecoder(myUri, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
}
private void Grid_MouseEnter(object sender, MouseEventArgs e)
{
mouseIn = true;
Thread th = new Thread(() =>
{
while (opasity < 100 && mouseIn)
{
System.Threading.Thread.Sleep(25);
opasity++;
try
{
this.Dispatcher.Invoke(new Action(delegate()
{
img_forward.Opacity = opasity / 100;
img_exit.Opacity = opasity / 100;
img_backward.Opacity = opasity / 100;
}));
}
catch
{
//the thread was probably aborted
}
}
});
threadList.Add(th);
th.Start();
}
private void Grid_MouseLeave(object sender, MouseEventArgs e)
{
mouseIn = false;
Thread th = new Thread(() =>
{
while (opasity > 0 && !mouseIn)
{
System.Threading.Thread.Sleep(25);
opasity--;
try{
this.Dispatcher.Invoke(new Action(delegate()
{
img_forward.Opacity = opasity / 100;
img_exit.Opacity = opasity / 100;
img_backward.Opacity = opasity / 100;
}));
}
catch
{
//the thread was probably aborted
}
}
});
threadList.Add(th);
th.Start();
}
private void img_backward_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (ImageNumber - 1 < 0)
ImageNumber = Classes.DataHouse.images.Count-1;
else
ImageNumber--;
Uri myUri = new Uri(Classes.DataHouse.images[ImageNumber], UriKind.RelativeOrAbsolute);
decoder2 = new GifBitmapDecoder(myUri, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
}
private void img_exit_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
foreach (Thread th in threadList)
{
while(th.ThreadState == ThreadState.Running)
th.Abort();
}
programClosed = true;
this.Close();
}
}
}
Unfortunately, animated GIF images were somewhat of an oversight when Microsoft developed WPF, so there is no 'built in' functionality that handles this. There are several working examples found online, but you should still be aware that code needs to be run on the UI thread to animate these images.
Therefore, certain situations, such as trying to use a 'busy' GIF during loading will not work because the UI thread will already be busy.
Having announced that disclaimer, you can find some working code that will do what you want in Thomas Levesque's GitHub project at the link below:
thomaslevesque/WpfAnimatedGif
Using a Thread
public MainWindow()
{
InitializeComponent();
Uri myUri = new Uri(#"Images\2b3601fe2b62e87481bd301da52a2182.gif", UriKind.RelativeOrAbsolute);
GifBitmapDecoder decoder2 = new GifBitmapDecoder(myUri, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
BitmapSource bitmapSource2;
int frameCount = decoder2.Frames.Count;
Thread th = new Thread(() => {
while (true)
{
for (int i = 0; i < frameCount; i++)
{
this.Dispatcher.Invoke(new Action(delegate()
{
bitmapSource2 = decoder2.Frames[i];
image.Source = bitmapSource2;
}));
System.Threading.Thread.Sleep(100);
}
}
});
th.Start();
}
This only works with physical files that aren't compiled as Resources, etc.
Try to change your image resource Build Action to Content or something similar, instead of Resource (And activate Copy to Output Directory, maybe?).
I managed to get a sample working that way. The key is that you must reference a physical file in some folder or internet URL.
For more info, check this out: https://social.msdn.microsoft.com/Forums/vstudio/en-US/93d50a97-0d8d-4b18-992e-cd3200693337/how-to-use-an-animated-gif?forum=wpf

How to not block the UI while doing time consuming work which DOES Involve accessing UI Controls?

I have a method which is a time consuming one and therefore I have been trying to implement a BackgroundWorker, but it does not allow accessing UI controls which I have read and tried (hacking it) but to no avail.
What my method does: Creates a new BitmapImage, sets the source local or streamed (the parameter), writes it to a new WriteableBitmap, which is used for ConvertToGrayscale and then saves the BW Copy to IsolatedStorage in a folder.
So all this happens quite fast. But, only when I have say less than 25 Source Images. If I have about 100+ Images, this takes considerably long like 20 seconds or more and therefore, I would like to show a ProgressBar in the same PhoneApplicationPage but I have been struggling with how to not block the UI and show the ProgressBar while the method is doing its work.
This is the code that I have:
void GetImages()
{
if (!myIsolatedStorage.DirectoryExists("ImagesBW") && !_appsettings.Contains("_update"))
{
myIsolatedStorage.CreateDirectory("ImagesBW ");
for (int i = 0; i < coll.Desserts.Count; i++)
{
BitmapImage bmp = new BitmapImage();
bmp.CreateOptions = BitmapCreateOptions.None;
if (coll.Desserts[i].HasAssociatedImage)
{
bmp.SetSource(coll.Desserts[i].GetImage());
WriteableBitmap wb = new WriteableBitmap(bmp);
ConvertToGrayscale(wb);
BitmapImage bit = ConvertWBtoBI(wb);
SaveBWCopy(bi, i.ToString());
}
else
{
bmp.UriSource = new Uri("/Assets/Images/MissingArt.png", UriKind.Relative);
WriteableBitmap wb = new WriteableBitmap(bmp);
ConvertToGrayscale(wb);
BitmapImage bit = ConvertWBtoBI(wb);
SaveBWCopy(bi, i.ToString());
}
}
_appsettings["_firstLaunch"] = "false";
_appsettings.Save();
}
else if (myIsolatedStorage.DirectoryExists("ImagesBW ") && _appsettings.Contains("_update"))
{
string[] files = myIsolatedStorage.GetFileNames("ImagesBW/*");
for (int s = 0; s < files.Length; s++)
{
myIsolatedStorage.DeleteFile("ImagesBW/" + s + ".jpg");
}
myIsolatedStorage.DeleteDirectory("ImagesBW");
myIsolatedStorage.CreateDirectory("ImagesBW");
for (int i = 0; i < coll.Desserts.Count; i++)
{
BitmapImage bmp = new BitmapImage();
bmp.CreateOptions = BitmapCreateOptions.None;
if (coll.Desserts[i].HasAssociatedImage)
{
bmp.SetSource(coll.Desserts[i].GetImage());
WriteableBitmap wb = new WriteableBitmap(bmp);
ConvertToGrayscale(wb);
BitmapImage bit = ConvertWBtoBI(wb);
SaveBWCopy(bi, i.ToString());
}
else
{
bmp.UriSource = new Uri("/Assets/Images/MissingArt.png", UriKind.Relative);
WriteableBitmap wb = new WriteableBitmap(bmp);
ConvertToGrayscale(wb);
BitmapImage bit = ConvertWBtoBI(wb);
SaveBWCopy(bi, i.ToString());
}
}
_appsettings.Remove("_update");
_appsettings.Save();
}
btnStart.IsEnabled = true;
}
Backgroundorker is your best bet. There are huge amount of resources on the web how to implement it. But the idea behind is that Backgroundworker runs in a separate thread from the UI. You can access main thread UI by two methods, a delegate or through the Backgroundworker's ProgressChanged method, which gives access to you UI thread.
Lets say you have this.
public MainWindow()
{
InitializeComponent();
//if you want to disable you button you can do it here
BackgroundWorker _bw = new BackgroundWorker();
_bw.DoWork += new DoWorkEventHandler(_bw_DoWork);
_bw.WorkerReportsProgress = true;
_bw.ProgressChanged += new ProgressChangedEventHandler(_bw_ProgressChanged);
_bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(_bw_RunWorkerCompleted);
_bw.RunWorkerAsync();
//or here
//Display progress bar here too
}
void _bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
//this can give you access to the UI after work is completed
// to check that everything is ok or hide progress bar
}
void _bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
//this will give you access to the UI in the middle of you work like update progress bar
}
void _bw_DoWork(object sender, DoWorkEventArgs e)
{
//actual work here including GetImages
//work work work
GetImages();
}
In you getimages method after the SaveBWCopy you can add this to update the progress bar
_bw.ReportProgress(int progress )
//progress is the percentage you want to send to progress bar to display that is going to be in the e eventargument you passed.

Simple C# application eating memory

Alright so basicly I have this simple application running in system tray that has one timer. Every tick it performs a check to see if a given directory and file exists, and based on the result it changes its icon.
The problem is every single timer tick the memory for the application raises ~100kb. I currently have it running for about 5 mins and it already uses 40MB of memory, which is unacceptable for such "micro" application.
Here's my code:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.IO;
using System.Reflection;
using System.Windows.Forms;
namespace Tray
{
public partial class Main : Form
{
string drive = "C:\\";
string file = "test.txt";
System.Drawing.Image imgRed = Image.FromFile("res\\icon-red.png");
System.Drawing.Image imgOrange = Image.FromFile("res\\icon-orange.png");
System.Drawing.Image imgGreen = Image.FromFile("res\\icon-green.png");
System.Drawing.Icon icoRed = System.Drawing.Icon.ExtractAssociatedIcon("res\\icon-red.ico");
System.Drawing.Icon icoOrange = System.Drawing.Icon.ExtractAssociatedIcon("res\\icon-orange.ico");
System.Drawing.Icon icoGreen = System.Drawing.Icon.ExtractAssociatedIcon("res\\icon-green.ico");
public Main()
{
InitializeComponent();
}
public static string ShowPrompt(string text, string caption)
{
Form prompt = new Form();
prompt.Width = 500;
prompt.Height = 150;
prompt.Text = caption;
Label textLabel = new Label() { Left = 50, Top = 20, Text = text };
TextBox textBox = new TextBox() { Left = 50, Top = 50, Width = 400 };
Button confirmation = new Button() { Text = "Ok", Left = 350, Width = 100, Top = 70 };
confirmation.Click += (sender, e) => { prompt.Close(); };
prompt.Controls.Add(confirmation);
prompt.Controls.Add(textLabel);
prompt.Controls.Add(textBox);
prompt.ShowDialog();
return textBox.Text;
}
public void updateInfo(){
this.statusDrive.Text = "Drive [" + drive + "]";
this.statusFile.Text = "File [" + drive + file + "]";
}
public void exec(){
int status = 0;
this.trayIcon.Text = "[Drive - ";
if (Directory.Exists(drive)){
this.statusDrive.Text += " - OK";
this.statusDrive.Image = imgGreen;
status++;
this.trayIcon.Text += "OK] ";
} else{
this.statusDrive.Text += " - FAIL";
this.statusDrive.Image = imgRed;
this.trayIcon.Text += "FAIL] ";
}
this.trayIcon.Text += "[File - ";
if (File.Exists(drive + file))
{
this.statusFile.Text += " - OK";
this.statusFile.Image = imgGreen;
status++;
this.trayIcon.Text += "OK] ";
}
else
{
this.statusFile.Text += " - FAIL";
this.statusFile.Image = imgRed;
this.trayIcon.Text += "FAIL] ";
}
switch (status)
{
case 2:
this.Icon = icoGreen;
this.trayIcon.Icon = icoGreen;
this.status.Image = imgGreen;
break;
case 1:
this.Icon = icoOrange;
this.trayIcon.Icon = icoOrange;
this.status.Image = imgOrange;
break;
case 0:
default:
this.Icon = icoRed;
this.trayIcon.Icon = icoRed;
this.status.Image = imgRed;
break;
}
}
private void Form1_Load(object sender, EventArgs e)
{
this.Hide();
}
private void timer1_Tick(object sender, EventArgs e)
{
updateInfo();
exec();
}
private void chDrive_Click(object sender, EventArgs e)
{
this.drive = ShowPrompt("Enter drive path", "Change drive");
}
private void chFile_Click(object sender, EventArgs e)
{
this.file = ShowPrompt("Enter new file path:", "Change file");
}
private void exitToolStripMenuItem_Click(object sender, EventArgs e)
{
Application.Exit();
}
}
}
I already tried to optimize the app by preloading the icons and images into variables and assigning those to the appropriate properties, however this didn't solve my problem.
Also, note that I managed to hide my main window by doing this (in Program.cs):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
namespace Tray
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Main mForm = new Main();
Application.Run();
}
}
}
UPDATE
I just noticed that the memory usage climbs up to 50MB and drops to 20MB afterwards (and goes up again). Is this something I can possibly address or is it a windows "issue"?
I'm going to take a stab at it being the string concatenations happening once a second. Consider using a StringBuilder. 40MB is nothing though really.
RE: Your update. The Garbage Collector is reclaiming the memory as it sees fit.
You never appear to be disposing your form correctly in ShowPrompt, so I'd imagine this is your problem.
Because a form displayed as a dialog box is not closed, you must call the Dispose method of the form when the form is no longer needed by your application.
ShowDialog
Some points that could cut down on memory usage:
Try to prebuild all those strings you're building in exec(). It looks like they're all runtime constants, but you build them every tick instead of building them once when the application starts. If this isn't possible, use StringBuilder instead of +=.
Only change properties on controls (icon, trayText, etc) if there has been a change. I.E. if tray text is already "[Drive C:\ - OK]", don't set its value again to "[Drive C:\ - OK]" next tick.
Garbage Collector does all the work of memory management for you. Temporary rise in memory doesn't always mean that there is a memory leak. It may come down when the GC collects memory. In case you suspect that there are memory leaks you need to do memory profiling which is no easy job. You need to read into this article for steps that you can take to find out the problem in your code. Alternatively, there are multiple tools avaiable in the market to do this job for you. You can use Ants Profiler of Red Gate, memprofiler amongst others.
One thing you might consider is rather than using a timer why not use the FileSystemWatcher and attach to events:
var watcher = new FileSystemWatcher("somepath");
watcher.Deleted += (sender, eventArgs) => { };
watcher.Changed += (sender, eventArgs) => { };
watcher.Error += (sender, eventArgs) => { };
watcher.Renamed += (sender, eventArgs) => { };
I also agree that you should be disposing of the forms once you're done with them.

Threading a Function in c# is not Working

I have a problem with this code. The function is playing a music track, so it takes a while to finish executing.... However, even through it is threaded, it does not return untill it is done, holding up the rest of the program. Can I have the function exit so that the program continues but have the music keep on it's own thread. Any solutions are welcome.
using System;
using Gtk;
using NAudio;
using NAudio.Wave;
using System.Threading;
public class Trackbox {
public static void Main() {
Application.Init();
//Create the Window
Window myWin = new Window("Trackbox");
myWin.SetIconFromFile("Assets//logo.png");
myWin.Resize(200, 100);
//Add the label to the form
//myWin.Add(myLabel);
Button playButton = new Button("Play Sound");
//This when playwav is called here, the rest of the application waits for it to finish playing
playButton.Clicked += new EventHandler(playWav);
myWin.Add(playButton);
myWin.DeleteEvent += delegate { Application.Quit(); };
//Show Everything
myWin.ShowAll();
Application.Run();
}
private static void playWav(object sender, EventArgs e)
{
var soundFile = #"C:\sound.wav";
using (var wfr = new WaveFileReader(soundFile))
using (WaveChannel32 wc = new WaveChannel32(wfr) { PadWithZeroes = false })
using (var audioOutput = new DirectSoundOut())
{
audioOutput.Init(wc);
audioOutput.Play();
while (audioOutput.PlaybackState != PlaybackState.Stopped)
{
Thread.Sleep(20);
}
audioOutput.Stop();
}
}
}
Thanks for the help. If you have any ideas please post.
Your playWav is executed on the same thread as your UI is running on. That is why your UI is blocked.
You can start a new thread like this:
private volatile bool _QuitThread;
private void playWav(object sender, EventArgs e)
{
_QuitThread = false;
Thread thread = new Thread(playWavThread);
thread.Start();
}
// This method should be called when the music should stop. Perhapse when a button has been pressed.
private void StopTheMusic()
{
_QuitThread = true;
}
private void playWavThread()
{
var soundFile = #"C:\sound.wav";
using (var wfr = new WaveFileReader(soundFile))
using (WaveChannel32 wc = new WaveChannel32(wfr) { PadWithZeroes = false })
using (var audioOutput = new DirectSoundOut())
{
audioOutput.Init(wc);
audioOutput.Play();
while (!_QuitThread && audioOutput.PlaybackState != PlaybackState.Stopped)
{
Thread.Sleep(20);
}
audioOutput.Stop();
}
}
EDIT
At request, I added code to quit the thread.
DirectSoundOut already creates its own playback thread. Get rid of the Thread.Sleep altogether which is blocking your thread and simply call Play. Subscribe to the PlaybackStopped event to detect when playback has finished. The audioOutput would need to be a class member so you could Dispose it after playback had finished.

Categories

Resources