January 2005 - Posts
I just started a new ASP.NET web application and because I'm quit lazy (or efficient?) I reused some of the code from a previous web project. The code had some methods like GetLanguage and SetLanguage that allowed me to make the site multilanguage based on .NET resource files (*.resx). However the code had one big drawback, each time I had to write code like:
private void Page_Load(object sender, System.EventArgs e)
{
if (!IsPostBack)
{
//Translate Controls
lblTitle.Text = Petfood.Context.rm.GetString("lblTitle");
lblPassword.Text = Petfood.Context.rm.GetString("lblPassword");
lblRemember.Text = Petfood.Context.rm.GetString("lblRemember");
lblIfNew.Text = Petfood.Context.rm.GetString("lblIfNew");
...
}
}
In this example the Class Context, provided a static method called rm, which returned a ResourceManager object responsible for retrieving the propper values from the right resource assembly.
As developer you don't like to write repetetive code like this, so I re-engeneered the code a bit. I compiled the result into a seperate assembly in order to improve the reuseability.
Now I have to write only one line of code:
private void Page_Load(object sender, System.EventArgs e)
{
if(!IsPostBack)
{
// Translate content of the controls to the current language
com.dotnet6.Localization.TranslateControls("Dauby.strings", this);
}
}
This method will recursivly look for all child controls (except user controls *) and will try to find a translation (in the resource files) based on the currently selected language.
* The reason why user controls are skipped is because they require the same code in their Page_Load event.
How do you apply this into your website, we'll just follow these three steps:
- Add a reference to ASPNET.i18n to your project
- Initialize the locazation
- Add Resource files to your project, containing the translations
Here's what happens
Step 1
(I assume we can skip step 1, at least I hope so)
Step 2
Again one line of code required in the global.asax:
protected void Application_BeginRequest(Object sender, EventArgs e)
{
com.dotnet6.Localization.InitializeLocalization();
}
Basically this is what happens behind the scenes:
Step 3
The last thing you need to do is to add to resource files which will contain the actual translations. So add a Assemby Resource File and call it for example strings.resx.This is where your main language translations will be stored. if you want other languages, just add additional files like strings.nl-BE.resx or strings.fr-BE.resx.
Now you need to add content to these files that will be mapped to the controls, note that you have to use this naming convention:
.
So, when you want to set the text of lblFirstName label of the default.aspx web form, you need to add default.aspx.lblFirstName to all the resource files and provide the translations.
The value of the element in the resource file will be set to the control as described below
- Label -> Text property
- LinkButton -> Text property
- Image -> ImageUrl
- ...
So, that's it. As usual feel free to post any comments or remarks.
download the assembly
download the Visual Studio 2003 project
- Erik
This minor release doesn't provide so much additional features, it is rather some internal stuff that has been improved.
We've done:
- Upgraded to the new Application Settings like mentioned in my last article (http://dotnet6.com/archive/2005/01/07/181.aspx)
- Earlier release made an async call to start the collecting process. All the aspects concerning this async call (including the marshaling) was coded on the wizards' GUI. Now a Collector class has been created who manages the collecting process and hides all threading aspects from the GUI. Later when we'll create an advanced mode, we can take advantage of this.
- When the user requested to cancel the collect process, iCollector will stop processing as soon as possible. So all running processes, get a request to stop their activities. Since some processes are command line utilities running on their own thread, we nicelly wait until they are finished. Whenever a collect is incomplete, iCollector will try to remove any intermediate files or directories.
- Renamed the Application Settings to WizardApplicationSettings. This way, we can keep different settings in case we run the wizard or advanced mode.
You can download the binaries here
Get the source code (Visual Studio 2003 project)
Check out iCollector project on gotdotnet
Read the full article on iCollector here
Read the release notes
- Erik
In order to open up the development, we decided to put the iCollector project on gotdotnet. it also improved our development with several people on different locations.
Check out iCollector on GotDotNet.com
- Erik
This posting continues on a post written earlier in December 2004.
Read "Application Settings - Part I" here
Download the source code here (Visual Studio 2003)
The solution presented in part one has in my opinion two drawbacks:
Problem 1
During the load of the data from disk, it is mandatory for the designer of the derived ApplicationSettings class to assign all loaded values correctly in an overriden LoadSettings method. In other words, unlike the Saving of Application Settings, additional plumbing is requested from the developer.Unfortunately so far; I didn't find any suitable solution for this one (yet).
Problem 2
When you provide (like in the example project) in the constructor of your ApplicationSettings class the Application.LocalUserAppDataPath, as folder location, it will look like:
C:\Documents and Settings\erikl\Local Settings\Application Data\dotnet6.com\AppSettings\1.0.3.0
Notice the complete version number at the end of the file. Now suppose your client application contains the following statement in the AssemblyInfo.cs file:
[assembly: AssemblyVersion("1.0.3.*")]
In this case, each time you build the application you will get a new location for your settings like:
C:\Documents and Settings\erikl\Local Settings\Application Data\dotnet6.com\AppSettings\1.0.3.25565
C:\Documents and Settings\erikl\Local Settings\Application Data\dotnet6.com\AppSettings\1.0.3.25601
So the numbers change with each new compile, which is verry often when your in debug mode dveloping the application. So whenever you start your application after a compile, it will find no application settings anymore. The solution would be to look during the load of the settings to any previous application settings and "import" them. This solution sounds ok but requires attention to the following two points:
1. When you have a lot of different version numbers (for example during development time) searching for the last config file isn't easy, especially since the build (last) part of the version is not an ever increasing number (well it is almost).
2. What happens when you change the layout of your Application Settings class, by adding and/or removing properties? Will the loading of the data using serialization still work?
Let's look at the first problem; I said it isn't easy to look for previous versions of application settings, so it makes the task even more fun. (and then again, wat is easy and what not!?). Here are the three steps to solve this one:
1. Basically if no Application Settings are found, we need to import older application settings. This is how the new LoadSettings of the ApplicationSettings class would look like now. (The new code is in bold)
public virtual bool LoadSettings()
{
FileStream fileStream = null;
FileInfo fi = null;
XmlSerializer serializer = null;
try
{
// Create an XmlSerializer for the ApplicationSettings type.
serializer = new XmlSerializer(this.GetType());
fi = new FileInfo(_configPath);
// If the config file exists, open it.
if(fi.Exists)
{
// Create a new instance of the ApplicationSettings by
// deserializing the config file.
fileStream = fi.OpenRead();
_appSettingsBase = (ApplicationSettings)serializer.Deserialize(fileStream);
return true;
}
else
{
//Look for old application settings files and import them
if (ImportOldApplicationSettings())
{
fi = new FileInfo(_configPath);
// Create a new instance of the ApplicationSettings by
// deserializing the config file.
fileStream = fi.OpenRead();
_appSettingsBase = (ApplicationSettings)serializer.Deserialize(fileStream);
return true;
}
else
return false;
}
}
catch(Exception ex)
{
throw new ApplicationException("Error while loading the application settings\n\nError details: " + ex.Message);
}
finally
{
// If the FileStream is open, close it.
if(fileStream != null)
{
fileStream.Close();
}
}
}
2. So how do we import any new application settings? There are three main steps involved here:
A. Get the newest of all old config files we can find.
//Get the last Config file from previous releases
string lastOldConfigFile = FindLastOldConfigFile();
B. Raise an event to inform the client application that older application settings can be imported in lack of any existing ones. The event that is raised passes an ImportOldAppSettingsEventArgs parameter to the event handler on the client application. Hereby the client application has the opportunity to set the Cancel property of these event argument to true, indicating to cancel the import of older application settings.
//Did the client registered this event
if (OldApplicationSettingsFound != null)
{
//Raise event in order to let the clients decide to import the old settings or not
ImportOldAppSettingsEventArgs eventArgs = new ImportOldAppSettingsEventArgs(lastOldConfigFile);
OldApplicationSettingsFound(this, eventArgs);
//Collect the Cancel value from the eventargs that the client has set
//if the client set the Cancel property of the eventArgs to "true", then
//we don't want to import the file
if (eventArgs.Cancel)
return false;
}
C. Import the old application settings, which is nothing more than copying the config file to the destination where our current application should be stored.
//Import the config file by copying it to the current version dir
File.Copy(lastOldConfigFile, _configPath);
So the complete method looks like this:
///
/// This method will look for old application settings stored in other places.
/// If there are none, ot will return false. If old Application Settings were
/// found, it will import the file into the current App. Settings location.
///
private bool ImportOldApplicationSettings()
{
try
{
//Get the last Config file from previous releases
string lastOldConfigFile = FindLastOldConfigFile();
//Exit when no old config file was found
if (lastOldConfigFile == String.Empty)
return false;
//Did the client registered this event
if (OldApplicationSettingsFound != null)
{
//Raise event in order to let the clients decide to import the old settings or not
ImportOldAppSettingsEventArgs eventArgs = new ImportOldAppSettingsEventArgs(lastOldConfigFile);
OldApplicationSettingsFound(this, eventArgs);
//Collect the Cancel value from the eventargs that the client has set
//if the client set the Cancel property of the eventArgs to "true", then
//we don't want to import the file
if (eventArgs.Cancel)
return false;
}
//Import the config file by copying it to the current version dir
File.Copy(lastOldConfigFile, _configPath);
return true;
}
catch
{
//ignore the error and indicate we were unable to get any previous config files
return false;
}
}
3. In the last step, we'll show you how to retrieve the most recent application settings to use for our import. I splitted this functionality into two other private operations called FindLastOldConfigFile and GetConfigFileFromFolder. Let's look at both:
The FindLastOldConfigFile method can be changed to whatever algorithm you like, but over here I used an approach where I would look into all sibling folders of the folder where the application settings should normally be. Then from all those folders, I will look for config files and add them to a SortedList using the date as key. In order to use a date as a sort key, I used the SortableDateTimePattern, which result into yyyy-MM-dd T HH:mm:ss which is by nature sortable. Then it was really easy to take the last item from the SortedList.
private string FindLastOldConfigFile()
{
//Get a list of folders where config files could exist
DirectoryInfo currentVersionConfigFolder = new DirectoryInfo(_configFolder);
DirectoryInfo parentVersionConfigFolder = currentVersionConfigFolder.Parent;
DirectoryInfo[] olderVersionConfigFolders = parentVersionConfigFolder.GetDirectories();
//Search for config files within those folders
SortedList configFiles = new SortedList();
foreach(DirectoryInfo olderVersionConfigFolder in olderVersionConfigFolders)
{
FileInfo configFile = GetConfigFileFromFolder(olderVersionConfigFolder, _configFile);
if(configFile != null)
{
//Add this to the sorted list using the formatted date as the key
//Note that the date format is based on:
// SortableDateTimePattern (based on ISO 8601) using local time
// therefore we can use the date to sort our found config files
configFiles.Add(configFile.LastAccessTime.ToString("s"), configFile);
}
}
//Return the newest one of the found config files
//Since we use a sorted list using a key on the file's LastAccessTime
//we just need to use the last object in the sorted list.
if (configFiles.Count > 0)
{
FileInfo lastOldConfigFile = (FileInfo)configFiles.GetByIndex(configFiles.Count-1);
return lastOldConfigFile.FullName;
}
else
return String.Empty;
}
So, the last method is GetConfigFileFromFolder, which is called by the FindLastOldConfigFile method. It will try to find a config file in the given folder and if it succeeds it will return a FileInfo object pointing to the found config file. This method wraps any possible exceptions when accessing the folders.
If you stick to the example application and use the Application.LocalUserAppDataPath to store the application settings you will not run into trouble, however if you decide to put your config file on for example "c:\Temp" then all sibling directories will be scanned for a config file. Since the 'System Volume Information' is also considered as a folder you will get an exception when trying to look for a config file in this folder.
private FileInfo GetConfigFileFromFolder(DirectoryInfo Folder, string ConfigFile)
{
try
{
//Get the configFile
return Folder.GetFiles(_configFile)[0];
}
catch
{
//Ignore any unaccessible folders like System Volume Information,
//or folders without user access rights.
return null;
}
}
Conclusion
This improvement will be very interesting during development time and when you provide a new version of your application to the users. However especially during development time, when you do a lot of builds, a huge number of directories could be created, so keep an I on it and clean them up regularly.
Download the source code here (Visual Studio 2003)
- Erik
Download Source code here (Visual Studio 2003)
When I was developing on one of the features of iCollector, I stumbled into a threading issue. When I detected the situation, I started searching in different directions in the .Net framework for some built in support, with little result.
Problem
So, the problem is basically how to get the details of Exceptions thrown in worker threads on to the main or UI thread. Most of you who are working with multi threaded windows applications know that the rule of thumb is:
"don't access forms or controls from a different thread, than the one they were created on"
So, if you set for example a Text property from a TextBox from a worker thread, you will run into unexpected results. However porting the application to Visual Studio 2005, you will get an error similar to:
"Illegal cross-thread operation: control x accessed from a thread other then the thread it was created on."
So, the goal is to access the form or control from the UI thread which can be done by marshaling to the UI thread. A lot of information can be found on the Internet on this, explaining you to use the InvokeRequired property to test if marshaling is required and then use Invoke to marshal a delegate from the worker thread to the UI thread. For example:
Let's get back to the problem. Suppose we have an exception in a worker thread and we want to show the details in a TextBox, then we need to use Marshal as explained above. Now, in the example below, I will create a class Worker, that performs some work. Since the Worker class takes care of the threading, I find it reasonable that it also takes care of any marshaling.
My Solution
Whenever an exception is thrown in a worker thread, I handle this exception and signal the client application by firing an event. The event passes the Exception that occurred on the worker thread via its EventArgs.
I will build the sample application step by step:
The Worker Class
1. Create a Worker class and pass a reference to the calling Form in the constructor.
using System;
using System.Threading;
namespace ThreadingExceptions
{
public class Worker
{
//Private Members
private System.Windows.Forms.Control _client;
///
/// Create a worker object
///
/// The caller or client of this object
public Worker(System.Windows.Forms.Control Client)
{
//Store reference to the client form
//We need this in order to marshall to the UI thread.
_client = Client;
}
}
}
2. Add a Work method to this class that will be called by the client. This method will take care of the creation of the threading stuff.
///
/// Create a worker thread and run some task on it.
///
public void Work()
{
//Create a new thread
Thread workerThread = new Thread(new ThreadStart(DoSomething));
workerThread.Name = "Worker Thread";
workerThread.Start();
}
3. Add another method called DoSomething that will actually perform the code you want to run on a different thread. Suppose that over here an Exception is thrown, so let's simmulate this.
public void DoSomething()
{
//Some code goes here ...
throw new ApplicationException("Something has gone wrong");
}
4. Before continuing with the Exception, we'll first define an event we can raise in which we can pass our Exception. Start by defining the EventHandler delagate and the event declaration. Note that I'm using the ThreadExceptionEventArgs, because its signature is just right for our situation here. ThreadExceptionEventArgs inherits from EventArgs and adds one additional property Exception.
//Delegate and event declaration for raising an WorkerException event to the client
public delegate void WorkExceptionEventHandler(object sender, ThreadExceptionEventArgs e);
public event WorkExceptionEventHandler WorkException;
5. Next we'll write a method that will raise the event. The goal is to call this method from the UI thread.
///
/// This method will fire the event towards the client.
/// Note that this method runs on the UI thread.
///
private void OnWorkException(Exception Exception)
{
if (WorkException != null)
WorkException(this, new ThreadExceptionEventArgs(Exception));
}
We'll now add code to handle the exception, marshal to the UI thread and raise the exception event to the client. To marshal to the UI thread, we will use the Invoke method of the _client variable (which references the client/calling form). The first parameter of the Invoke takes a delegate to the method that will be executed on the UI thread and the second parameter is an array to pass any extra arguments.
6. So let's start by defining the delegate by adding this two lines to the Worker class.
//Delegate required for marshalling the exception to the UI thread
private delegate void DelegateOnWorkException(Exception Exception);
//Private Members
private DelegateOnWorkException _delegateOnWorkException;
7. Now create an instance of the delegate and let it point to the OnWorkException method we created earlier. So add this line in the constructor of the Worker class.
//The delegate that points to the method that will be executed on the UI thread
_delegateOnWorkException = new DelegateOnWorkException(OnWorkException);
8. The last step is the marshaling part, which we'll do when catching the Exception. So rewrite the DoSomething method like this:
public void DoSomething()
{
try
{
//Some code goes here ...
throw new ApplicationException("Something has gone wrong");
}
catch(Exception ex)
{
//marschall to the UI thread.
_client.Invoke(_delegateOnWorkException, new object[] {ex});
}
}
The Windows Client Application
Over here we show you a part of the client application. Just add a Button btnDoWork and a TextBox tbxResult on the form. Then add the code like below.
private void btnDoWork_Click(object sender, System.EventArgs e)
{
//Set name of current (UI) thread, to watch the threads in threads Window of Visuam Studio
System.Threading.Thread.CurrentThread.Name = "UI Thread";
//Create a new worker
Worker worker = new Worker(this);
//Hook up the exception event
worker.WorkException += new ThreadingExceptions.Worker.WorkExceptionEventHandler(OnWorkException);
//Start the work
worker.Work();
}
///
/// Event handler that is fired by the Worker when an exception occured.
///
private void OnWorkException(object sender, System.Threading.ThreadExceptionEventArgs e)
{
tbxResult.Text = "Unable to complete the work.\r\nError Details:" + e.Exception.Message;
}
Alternative
I once tried to go without passing a reference to the client, but then I had to write my own Invoke method. So I tried implementing the ISynchronizeInvoke interface, unfortunately this was out of my league. Perhaps somebody could shed a light on this one.
Conclusion
Perhaps this is not the best way to get to this result, but then again, I'm not the Threading expert. Anyway the solution works fine for me and hopefully for you to.
Download Source code here (Visual Studio 2003)
- Erik
[View other posts on iCollector]
After some christmas break where most of us ate and drank too much as usual, we bring you a next release of iCollector. Release 0.7 includes mainly one new feature, which is the collection of protected AAC files. So basically, when you work with iTunes, you most likely end up with a music library containing MP3 and AAC files. When you import your music from a CD, the songs turn into Unprotected AAC files and when you buy songs via the iTunes online shop, you'll get Protected AAC files.
Now, we're able convert all of them to MP3 and collect them with this release of iCollector.
Download iCollector 0.7
Download iCollector Source code 0.7
Read the full article on iCollector here
Read the release notes