Generate XML based Resource Files on the fly in ASP.NET 1.x applications


Introduction:

 

Generating resource files from WebForms & UserControls and adding multilingual support for applications in ASP.NET 2.0 is a piece of cake. You do that by selecting the "Generate Local Resource" command from the Visual Studio 2005 IDE Tools menu and fire up the resx generation for the default culture. It adds the page directive and creates 'meta:resourcekey' attributes to the server controls throughout the page and makes corresponding entries in the resource files which act as endpoints to these attributes. These files are created under App_LocalResources folder if they're page-specific and App_GlobalResources if the resource is shared globally by the application.

 

But this is all a different story in ASP.NET 1.x applications. What if, the applications you're dealing with still runs on ASP.NET 1.1 and were asked to get your applications "world-ready" under strict timelines with no time and budget to port to 2.0? Well, as a developer, you would eventually look for solutions that could automate the entire resx generation process and keep them in sync with the UI. That is, whenever a control is added, modified or removed from the Pages/User Controls the resource files are re-created with the latest name/value pairs.

 

The Logic (in a nutshell):

 

The goal of this article is to demonstrate getting culture-specific values from the resources while simultaneously creating XML based resx files when a page is loaded. The code, which accomplishes this, must be kept in a common class, which runs in response to a particular event in the Page's life cycle without replicating it across all the webforms. To achieve this, you would usually want to create a custom base class that inherits from System.Web.UI.Page and override the OnInit event. This overridden event calls methods that recursively set the control's text properties by pulling appropriate values from the resource files through a Resource Wrapper. The ResourceWrapper uses ResourceManager class to retrieve values based on the current culture and also keeps track of control's resourceKeys and values by adding them to a Hashtable. The Control's ID is used as resource Key and its Text property as value. The recursive method in the BasePage also uses a Hashtable to store unique instances of ResourceWrapper objects based on the control's naming container. This ensures that each object holds its own collection of resourceKeys and values specific to their WebForms and User Controls.

 

The ResourceWrapper Class:

 

The ResourceWrapper internally uses ResourceManager to retrieve strings based on the current culture. It exposes two important methods, GetString and GenerateResourceFiles. The GetString method accepts two arguments, the control's ID as key and its default Text (from design time) as value, and returns the same Text if an exception is thrown or if the key is not found in the satellite assembly. The GenerateResourceFiles method calls a separate library that provides an implementation to write resx files.

 

public class ResourceWrapper

{

    private Hashtable _ResourceTable = new Hashtable();

    private ResourceManager _rm;

    private Boolean _isResxSyncEnabled = false;

 

    public ResourceWrapper(ResourceManager rm, Boolean isResxSyncEnabled)

    {

        this._rm = rm;

        _isResxSyncEnabled = isResxSyncEnabled;

    }

    /// <summary>

    /// This method retrieves the value from the ResourceManager class.

    /// And adds the key/value to the Hashtable if it needs to be synced up with Resx.

    /// </summary>

    /// <param name="key">Control's ID</param>

    /// <param name="value">Control's Text property</param>

    /// <param name="rm"></param>

    /// <returns></returns>

    public string GetString(string key, string value)

    {

        string retValue = string.Empty;

        try

        {

            retValue = _rm.GetString(key);

            if (retValue == null)

            {

                retValue = value;

            }

        }

        catch (Exception)

        {

            retValue = value;

        }

 

        finally

        {

            if (_isResxSyncEnabled)

            AddToResourceTable(_rm.BaseName + "." + key, value);

        }

        return retValue;

    }

 

    /// <summary>

    /// Adds unique resourceKeys and values to the Hashtable

    /// </summary>

    /// <param name="key"></param>

    /// <param name="value"></param>

    private void AddToResourceTable(string key, string value)

    {

        if (!_ResourceTable.Contains(key))

        _ResourceTable.Add(key, value);

    }

 

 

    /// <summary>

    /// Generate RESX files

    /// </summary>

    /// <param name="ResxDirectoryPath"></param>

    public void GenerateResourceFiles(string ResxDirectoryPath)

    {

        ResourceWriter.ResourceWriter.WriteAllResourceFiles(_ResourceTable, ResxDirectoryPath);

    }

}

 

The Base Class (BasePage.cs):

 

The Base class named BasePage inherits from System.Web.UI.Page and overrides the Page's OnInit. This ensures that OnInit events fired by any page in your application will be handled by this overridden method in the base class. This also addresses a common scenario where you want a control to render dynamic localized-text which, in this case, can be assigned to the control's text property after the OnInit event during the Page's Life Cycle. This method won't be hit if the page's codebehind class does not inherit from BasePage, as simple as that!

 

override protected void OnInit(EventArgs e)

{

    base.OnInit (e);

    ReadWriteResources((System.Web.UI.Control) Page);

}

 

The ReadWriteResources method calls the SetControlTextRecursive method. I've added a key "SyncResxFiles" in the web.config, which works as a switch to either enable or disable resource file creation. You can set the value to false once you're sure that all the Resx files are generated with right resourceKeys for the UI elements. 

 

public class BasePage : System.Web.UI.Page

{

    public ResourceWrapper _ResWrapper;

    private Hashtable _ResmgrsTable = new Hashtable();

    private Boolean _isResxSyncEnabled = false;

 

    /// <summary>

    /// Page's OnInit event fired anywher in your application is handled by this method.

    /// Make sure that the ASPX pages inherit from BasePage and NOT from System.Web.UI.Page

    /// </summary>

    /// <param name="e"></param>

    override protected void OnInit(EventArgs e)

    {

        base.OnInit(e);

        ReadWriteResources((System.Web.UI.Control)Page);

    }

 

    /// <summary>

    /// Once the recursion ends, the resource files are generated for the page if the switch in

    /// the config is set to true.       

    /// </summary>

    /// <param name="Container"></param>

    private void ReadWriteResources(System.Web.UI.Control Container)

    {

        AppSettingsReader appSettings = new AppSettingsReader();

        _isResxSyncEnabled = (Boolean)

        appSettings.GetValue("SyncResxFiles", typeof(Boolean));

 

        SetControlTextRecursive(Container);

 

        if (_isResxSyncEnabled)

        {

            IDictionaryEnumerator enumerator = _ResmgrsTable.GetEnumerator();

            while (enumerator.MoveNext())

            {

                ResourceWrapper rw = (ResourceWrapper)enumerator.Value;

                rw.GenerateResourceFiles(Server.MapPath(@"~\Resources"));

            }

        }

    }

 

    /// <summary>

    /// Recursive method to traverse all controls on the page.

    /// Including User Controls

    /// </summary>

    /// <param name="ControlName">Control Name without extension</param>

    private void SetControlTextRecursive(System.Web.UI.Control Container)

    {

        foreach (System.Web.UI.Control control in Container.Controls)

        {

            SetText(control);

            if (control.HasControls())

            {

                SetControlTextRecursive(control);

            }

        }

    }

 

    /// <summary>

    /// This method sets the Text property of a Server Control.

    /// The Hashtable holds ResourceWrapper instances for each NamingContainer.

    /// This way, the controls are separted based on the container if there are

    /// multiple/nested user controls on a single aspx page.

    /// </summary>

    /// <param name="c"></param>

    private void SetText(System.Web.UI.Control c)

    {

        ResourceWrapper rw = null;

        string containerName = c.NamingContainer.ToString().Replace("ASP.", string.Empty);

        if (_ResmgrsTable.Contains(containerName))

        {

            rw = (ResourceWrapper)_ResmgrsTable[containerName];

        }

        else

        {

            rw = new ResourceWrapper(new ResourceManager("AxiaUopClassOps.Resources." + containerName, Assembly.GetExecutingAssembly(), null), _isResxSyncEnabled);

            _ResmgrsTable.Add(containerName, rw);

        }

        //Only limited control types which has Text property is handled in this code.

        //e.g. Label, Button, RadioButton, HyperLink, RequiredFieldValidator, RegularExpressionValidator

        //You can always add more controls within the switch/case that needs to render culture-specific texts.

 

        switch (c.GetType().ToString())

        {

            case "System.Web.UI.WebControls.Label":

                Label label = (Label)c;

                label.Text = rw.GetString(label.ID, label.Text);

                break;

            case "System.Web.UI.WebControls.Button":

                Button b = (Button)c;

                b.Text = rw.GetString(b.ID, b.Text);

                break;

            case "System.Web.UI.WebControls.RadioButton":

                RadioButton rb = (RadioButton)c;

                rb.Text = rw.GetString(rb.ID, rb.Text);

                break;

            case "System.Web.UI.WebControls.HyperLink":

                HyperLink hyperlink = (HyperLink)c;

                hyperlink.Text = rw.GetString(hyperlink.ID, hyperlink.Text);

                break;

            case "System.Web.UI.WebControls.RequiredFieldValidator":

                RequiredFieldValidator rfv = (RequiredFieldValidator)c;

                rfv.ErrorMessage = rw.GetString(rfv.ID, rfv.ErrorMessage);

                break;

            case "System.Web.UI.WebControls.RegularExpressionValidator":

                RegularExpressionValidator rev = (RegularExpressionValidator)c;

                rev.ErrorMessage = rw.GetString(rev.ID, rev.ErrorMessage);

                break;

        }

    }

}

 

The ResourceCreator Class:

 

The ResourceCreator uses ResxResourceWriter class found in the System.Resources namespace. One important thing to be noted here is that System.Resources.ResxResourceWriter class which writes resources in XML format (*.resx) is found in the System.Windows.Forms assembly where as the System.Resources.ResourceWriter class which writes resources in binary format(*.resources) is found in MS common object runtime library (mscorlib.dll). So make sure that a reference to System.Windows.Forms.dll is added to your project if you want to create ".resx" files rather than ".resources" files outside a windows forms application.

 

public class ResourceCreator

{

    //The resource folder path where you want your .resx files to be created.             

    private static string ResourceDirectoryPath = @"C:\Inetpub\wwwroot\WebResxTest\Resources";

 

    public static void WriteAllResourceFiles(Hashtable ResourceCollection, string ResxDirectoryPath)

    {

        IDictionaryEnumerator enumerator = ResourceCollection.GetEnumerator();

        ResXResourceWriter resourceWriter = null;

        Boolean isCreated = false;

 

        while (enumerator.MoveNext())

        {

            string[] keyString = enumerator.Key.ToString().Split(Convert.ToChar("."));

            string controlName = keyString.GetValue(keyString.Length - 1).ToString();

            string pageName = keyString.GetValue(keyString.Length - 2).ToString();

            if (!isCreated)

            {

                resourceWriter = new ResXResourceWriter(ResourceDirectoryPath + "\\" + pageName + ".resx");

                isCreated = true;

            }

            resourceWriter.AddResource(controlName, enumerator.Value.ToString());

        }

        if (isCreated)

        {

            resourceWriter.Generate();

            resourceWriter.Close();

        }

    }

}

 

When all the resx files are generated, you have to manually include them into your project and rebuild the solution to create satellite assemblies.

 

Conclusion:

 

The solution I've presented here can be further refined and extended over to .NET 2.0 applications to setup Globalization/Internationalization/Localization framework for time crunching web applications.