Reputation: 901
I'm still quite new to C# and programming in general, but I've made a few application until now. Normal applications that only do one task and then quit are simple enough, but having a system for example take 500 pictures of users a day gave me a harder challenge.
My issue is about memory consumption in WPF. I have the following page, and when loaded it eats up more and more memory. I've tried to use memory analyzer tool and created some snapshots trying to resolve this issue. However, I have a hard time understanding when/how to dispose object and be sure that GC takes care of the rest. One of the pages I'm having trouble with in particular is this:
Page 2:
using EDSDKLib;
using PhotoBooth.Functions;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
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.Windows.Threading;
namespace PhotoBooth.Pages
{
/// <summary>
/// Interaction logic for Picture.xaml
/// </summary>
public partial class Picture : Page
{
int secondsToWait = 4;
DispatcherTimer dispatcherTimer;
Action<BitmapImage> SetImageAction;
ImageBrush bgbrush = new ImageBrush();
public Picture()
{
InitializeComponent();
// Define steps
Global.CreateSteps(Global.SelectedMenuOrder, this, ((MasterPage)System.Windows.Application.Current.MainWindow).StepsWindow);
// Create TempLocation
Directory.CreateDirectory(Settings.TempLocation);
// Handle the Canon EOS camera
Global.CameraHandler.ImageSaveDirectory = Settings.TempLocation;
SetImageAction = (BitmapImage img) => { bgbrush.ImageSource = img; };
// Configure the camera timer
dispatcherTimer = new DispatcherTimer();
dispatcherTimer.Tick += DispatcherTimer_Tick;
dispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 800);
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
// Subscribe to camera events
if (Global.CameraHandler != null)
{
Global.CameraHandler.LiveViewUpdated += CameraHandler_LiveViewUpdated;
Global.CameraHandler.ImageSaved += CameraHandler_ImageSaved;
Global.CameraHandler.CameraSDKError += CameraHandler_CameraSDKError;
}
// Start LiveView
try
{
Console.WriteLine(Global.CameraHandler.IsLiveViewOn);
if (!Global.CameraHandler.IsLiveViewOn)
{
CameraLiveView.Background = bgbrush;
Global.CameraHandler.StartLiveView();
}
}
catch (Exception)
{
// We cannot recover from that kind of errror. Reboot the application
CameraCrashHandler();
}
}
private void CameraTrigger_Click(object sender, RoutedEventArgs e)
{
// The user has clicked the trigger, change the layout
CameraTrigger.Visibility = System.Windows.Visibility.Collapsed;
CameraCountDown.Visibility = System.Windows.Visibility.Visible;
CameraTrigger.IsEnabled = false;
// Start the countdown
secondsToWait = 4;
dispatcherTimer.Start();
Global.WriteToLog("INFO", "Camera shutter pressed... waiting for camera to take picture!");
}
private void DispatcherTimer_Tick(object sender, EventArgs e)
{
// Handles the countdown
switch (secondsToWait)
{
case 4:
CameraTimer3.Foreground = new SolidColorBrush(Colors.White);
Global.PlaySound("pack://application:,,,/Resources/Audio/camera_beep.wav");
break;
case 3:
CameraTimer2.Foreground = new SolidColorBrush(Colors.White);
Global.PlaySound("pack://application:,,,/Resources/Audio/camera_beep.wav");
break;
case 2:
CameraTimer1.Foreground = new SolidColorBrush(Colors.White);
Global.PlaySound("pack://application:,,,/Resources/Audio/camera_beep.wav");
break;
case 1:
CameraTimer0.Source = new BitmapImage(new Uri("pack://application:,,,/Resources/Images/icon_cameraWhite.png"));
Global.CameraFlashEffect(((MasterPage)System.Windows.Application.Current.MainWindow).CameraFlash);
Global.CameraHandler.TakePhoto();
break;
case 0:
CameraTimer0.Source = new BitmapImage(new Uri("pack://application:,,,/Resources/Images/icon_cameraRed.png"));
CameraTimer1.Foreground = (SolidColorBrush)(new BrushConverter().ConvertFrom("#e8234a"));
CameraTimer2.Foreground = (SolidColorBrush)(new BrushConverter().ConvertFrom("#e8234a"));
CameraTimer3.Foreground = (SolidColorBrush)(new BrushConverter().ConvertFrom("#e8234a"));
dispatcherTimer.Stop();
break;
default:
break;
}
secondsToWait--;
}
private void Page_Unloaded(object sender, RoutedEventArgs e)
{
// Stop LiveView
if (Global.CameraHandler.IsLiveViewOn)
{
CameraLiveView.Background = null;
Global.CameraHandler.StopLiveView();
}
// Unsubscribe from events
Global.CameraHandler.LiveViewUpdated -= CameraHandler_LiveViewUpdated;
Global.CameraHandler.ImageSaved -= CameraHandler_ImageSaved;
Global.CameraHandler.CameraSDKError -= CameraHandler_CameraSDKError;
}
#region CameraHandler
void CameraHandler_CameraSDKError(string error)
{
// Handle known errors
Global.WriteToLog("ERROR", "CameraSDKError: " + error);
switch (error)
{
case "0x00008D01":
// Reset cameraTrigger for taking another photo
this.Dispatcher.Invoke((Action)(() =>
{
CameraTrigger.Visibility = System.Windows.Visibility.Visible;
CameraCountDown.Visibility = System.Windows.Visibility.Collapsed;
CameraTrigger.IsEnabled = true;
}));
break;
default:
CameraCrashHandler();
break;
}
}
void CameraHandler_ImageSaved(string img)
{
// Assign image to user
Global.PersonObject.Image = img;
// We have a image, let's navigate to the next page
this.Dispatcher.Invoke((Action)(() =>
{
NavigationService.Navigate(Global.FindPageByString(Global.NavigateManager(this, Functions.Enums.Navigation.Forward)));
}));
}
void CameraHandler_LiveViewUpdated(Stream img)
{
try
{
if (Global.CameraHandler.IsLiveViewOn)
{
using (WrappingStream s = new WrappingStream(img))
{
img.Position = 0;
BitmapImage EvfImage = new BitmapImage();
EvfImage.BeginInit();
EvfImage.StreamSource = s;
EvfImage.CacheOption = BitmapCacheOption.OnLoad;
EvfImage.EndInit();
EvfImage.Freeze();
Application.Current.Dispatcher.Invoke(SetImageAction, EvfImage);
}
}
}
catch (Exception ex)
{
Global.WriteToLog("ERROR", "LiveViewUpdated failed: " + ex.Message);
}
}
static void CameraCrashHandler()
{
// Camera cannot start
Global.WriteToLog("ERROR", "Unkown CameraSDKError. Automatic reboot needed");
MessageWindow mw = new MessageWindow("CameraErrorTitle", "CameraErrorMessage");
mw.ShowDialog();
// We cannot recover from that kind of errror. Reboot the application
System.Windows.Forms.Application.Restart();
System.Windows.Application.Current.Shutdown();
}
#endregion
}
}
Page 3:
using PhotoBooth.Functions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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 PhotoBooth.Pages
{
/// <summary>
/// Interaction logic for PreviewID.xaml
/// </summary>
public partial class PreviewID : Page
{
public PreviewID()
{
InitializeComponent();
// Define steps
Global.CreateSteps(Global.SelectedMenuOrder, this, ((MasterPage)System.Windows.Application.Current.MainWindow).StepsWindow);
// Load image and data
PreviewIDText = GetIDText(Global.PersonObject, PreviewIDText);
PreviewIDCard.Source = GetIDPhoto(Global.PersonObject);
PreviewIDPhoto.Background = new ImageBrush(new BitmapImage(new Uri(Global.PersonObject.Image)));
}
private void PreviewIDRetry_Click(object sender, RoutedEventArgs e)
{
Global.WriteToLog("INFO", "User did not approve the image, retry!");
NavigationService.Navigate(Global.FindPageByString(Global.NavigateManager(this, Functions.Enums.Navigation.Backward)));
}
private void PreviewIDAccept_Click(object sender, RoutedEventArgs e)
{
Global.WriteToLog("INFO", "User approved the image");
NavigationService.Navigate(Global.FindPageByString(Global.NavigateManager(this, Functions.Enums.Navigation.Forward)));
}
public TextBlock GetIDText(Functions.Enums.Person p, TextBlock tb)
{
tb.Text = "";
tb.FontSize = 24;
if (p.Affiliation == Functions.Enums.Affiliation.Employee)
{
// Ansatt
tb.Inlines.Add(new Run(p.FirstName + " " + p.LastName + Environment.NewLine) { FontWeight = FontWeights.Bold, FontSize = 30 });
tb.Inlines.Add(p.Title + Environment.NewLine);
tb.Inlines.Add(p.Department + Environment.NewLine);
tb.Inlines.Add("Ansatt nr: " + p.EmployeeNumber + Environment.NewLine);
}
else
{
// Student
tb.Inlines.Add("Last name: ");
tb.Inlines.Add(new Run(p.LastName + Environment.NewLine) { FontWeight = FontWeights.Bold });
tb.Inlines.Add("First name: ");
tb.Inlines.Add(new Run(p.FirstName + Environment.NewLine) { FontWeight = FontWeights.Bold });
tb.Inlines.Add("Date of birth: ");
tb.Inlines.Add(new Run("dd.mm.yyyy" + Environment.NewLine) { FontWeight = FontWeights.Bold });
tb.Inlines.Add("Studentnr: ");
tb.Inlines.Add(new Run("xxxxxx" + Environment.NewLine) { FontWeight = FontWeights.Bold });
}
return tb;
}
public BitmapImage GetIDPhoto(Functions.Enums.Person p)
{
BitmapImage result;
switch (p.Affiliation)
{
case PhotoBooth.Functions.Enums.Affiliation.Student:
result = new BitmapImage(new Uri("pack://application:,,,/Resources/Images/idcard_student.png"));
break;
case PhotoBooth.Functions.Enums.Affiliation.Employee:
result = new BitmapImage(new Uri("pack://application:,,,/Resources/Images/idcard_employee.png"));
break;
default:
result = new BitmapImage(new Uri("pack://application:,,,/Resources/Images/idcard_student.png"));
break;
}
return result;
}
private void Page_Unloaded(object sender, RoutedEventArgs e)
{
PreviewIDPhoto.Background = null;
}
}
}
While most of the Global. functions are used on all other pages, they seem to be fine as it's this page creating most of my troubles as far as I can tell.
Here's a screenshot of my memory performance test.
User navigates from page 1 to page 2 (or other pages), no issues as far as I can see. Memory usage seems stable.
User triggers a Canon camera with LiveView (page 2). Memory consumption goes up and down, but stable.
User takes image and get's a preview (page 3), retries (goes back to page 2), takes another, retries and so on...
Every time the code behind is loaded:
Are the BitmapImages causing this issue? If there's nothing obvious in that code, how should I proceed to test memory leaks?
Upvotes: 3
Views: 1990
Reputation: 901
According to the memory profiler I have found and fixed this issue. I still don't quite understand why, but it works. :)
The issue was the following line of code:
PreviewIDPhoto.Background = new ImageBrush(new BitmapImage(new Uri(Global.PersonObject.Image)));
Once I had that commented out I didn't have any memory issues anymore. However, I had a hard time rewriting this to work. Seems like user1690200 was right about freezing the image, but not quite the code he posted.
This worked for me:
BitmapImage image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.UriSource = new Uri(Global.PersonObject.Image);
image.DecodePixelWidth = 275; // Important as we do not want to load the whole image
image.EndInit();
image.Freeze(); // Call this after EndInit and before using the image.
PreviewIDPhoto.Source = image;
Upvotes: 0
Reputation: 28
I think it is because of the DispatcherTimer. Probably just stopping the timer isnt sufficient when unloading the page, because the WPF will still store a reference to your class.Try unregistering the event when unloading the page.
Upvotes: 1