Extending Resource-Provider for storing resources in the database

This post, I upgraded from the articles that published from IDesign group in the MSDN's Microsoft http://msdn.microsoft.com/en-us/library/aa905797.aspx. The reason I made it is because I need using this solution for my ASP.NET MVC project. This method is so old for now. So I start to introduce about my effort for modify it for fixed with my solution.

Up to  now, I also only stored the resources in Resource files with .resx extension (for example, LoginPage.en-US.resx, Category.en-US.resx,...). Resource files can be in Local Resources or Global Resources, this is meaning if you put it are Global Resources, then all pages in your project can access and use it for sharing, or else if you declare it as Local Resources, you only access it in the restricted scope only inside one page. But why do I want to storing it in the database? I can list it at here:

  • A site that has thousands of pages and multiple-thousand resource entries, using assembly resources might not be ideal. It will add to run-time memory usage, not to mention the increased number of assemblies loaded into the application domain
  • Database resources might also provide a more flexible and manageable environment for the localization process, for reducing duplicates, for intricate caching options, and for storing possibly larger blocks of content
  • Allocating resources to a database makes it possible to support more complicated hierarchies of translated content, where customers or departments might have customized versions of the text that is then also localized.

And the main reason that I store Resources in the Database is I want to pratice it. Now I start to implementing it.

The first step, I will created the database schema for stored resources. And the struture for it is:

I also input some demo data for this table:

For easily to understanding, I will put some Model Diagram at below:

 

As the Diagram above, I must implement the DBResourceReader and let to it for read the resources. Next I must implement the DBResourceProvider for injecting the Database Access layer into it (as here is a ResourceFacade, if you don't know about Facade pattern before, please go to http://en.wikipedia.org/wiki/Facade_pattern for references). And finally, I will built the DBResourceProviderFactory for get the DBResourceProvider instance that I just implemented in previous step (for Factory patterns you can reference at here http://en.wikipedia.org/wiki/Factory_method_pattern and http://en.wikipedia.org/wiki/Abstract_factory_pattern).

And there are a details of code:

+ DBResourceReader.cs

    /// <summary>
    /// Implementation of IResourceReader required to retrieve a dictionary
    /// of resource values for implicit localization. 
    /// </summary>
    public class DBResourceReader : DisposableBaseTypeIResourceReaderIEnumerable<KeyValuePair<stringobject>>
    {

        private ListDictionary m_resourceDictionary;

        public DBResourceReader(ListDictionary resourceDictionary)
        {
            Debug.WriteLine("DBResourceReader()");

            this.m_resourceDictionary = resourceDictionary;
        }

        protected override void Cleanup()
        {
            try
            {
                this.m_resourceDictionary = null;
            }
            finally
            {
                base.Cleanup();
            }
        }

        #region IResourceReader Members

        public void Close()
        {
            this.Dispose();
        }

        public IDictionaryEnumerator GetEnumerator()
        {
            Debug.WriteLine("DBResourceReader.GetEnumerator()");

            // NOTE: this is the only enumerator called by the runtime for 
            // implicit expressions

            if (Disposed)
            {
                throw new ObjectDisposedException("DBResourceReader object is already disposed.");
            }

            return this.m_resourceDictionary.GetEnumerator();
        }

        #endregion

        #region IEnumerable Members

        IEnumerator IEnumerable.GetEnumerator()
        {
            if (Disposed)
            {
                throw new ObjectDisposedException("DBResourceReader object is already disposed.");
            }

            return this.m_resourceDictionary.GetEnumerator();
        }

        #endregion

        #region IEnumerable<KeyValuePair<string,object>> Members

        IEnumerator<KeyValuePair<stringobject>> IEnumerable<KeyValuePair<stringobject>>.GetEnumerator()
        {
            if (Disposed)
            {
                throw new ObjectDisposedException("DBResourceReader object is already disposed.");
            }

            return this.m_resourceDictionary.GetEnumerator() as IEnumerator<KeyValuePair<stringobject>>;
        }

        #endregion
    }

+ DBResourceProvider.cs 

    /// <summary>
    /// Resource provider accessing resources from the database.
    /// This type is thread safe.
    /// </summary>
    public class DBResourceProvider : DisposableBaseTypeIResourceProvider
    {
        public string ClassKey { getprivate set; }
        public IResourceFacade ResourceFacade { getprivate set; }

        //resource cache
        private Dictionary<stringDictionary<stringstring>> _resourceCache = new Dictionary<stringDictionary<stringstring>>();

        /// <summary>
        /// Constructs this instance of the provider 
        /// supplying a resource type for the instance. 
        /// </summary>
        /// <param name="resourceType">The resource type.</param>
        public DBResourceProvider(string classKey) : this(IoC.GetInstance<IResourceFacade>())
        {
            Check.Assert(ResourceFacade != null"ResourceFacade instance is null");

            Debug.WriteLine(String.Format(CultureInfo.InvariantCulture, "DBResourceProvider.DBResourceProvider({0}", classKey));

            ClassKey = classKey;
            ResourceFacade.ResourceType = ClassKey;
        }

        public DBResourceProvider(IResourceFacade resourceFacade)
        {
            Check.Assert(resourceFacade != null"ResourceFacade instance is null");
            ResourceFacade = resourceFacade;
        }

        #region IResourceProvider Members

        /// <summary>
        /// Retrieves a resource entry based on the specified culture and 
        /// resource key. The resource type is based on this instance of the
        /// DBResourceProvider as passed to the constructor.
        /// To optimize performance, this function caches values in a dictionary
        /// per culture.
        /// </summary>
        /// <param name="resourceKey">The resource key to find.</param>
        /// <param name="culture">The culture to search with.</param>
        /// <returns>If found, the resource string is returned. 
        /// Otherwise an empty string is returned.</returns>
        public object GetObject(string resourceKey, CultureInfo culture)
        {
            Debug.WriteLine(String.Format(CultureInfo.InvariantCulture, "DBResourceProvider.GetObject({0}, {1}) - type:{2}", resourceKey, culture, this.ClassKey));

            if (Disposed)
            {
                throw new ObjectDisposedException("DBResourceProvider object is already disposed.");
            }

            if (string.IsNullOrEmpty(resourceKey))
            {
                throw new ArgumentNullException("resourceKey");
            }

            if (culture == null)
            {
                culture = CultureInfo.CurrentUICulture;
            }

            string resourceValue = null;
            Dictionary<stringstring> resCacheByCulture = null;
            // check the cache first
            // find the dictionary for this culture
            // check for the inner dictionary entry for this key
            if (_resourceCache.ContainsKey(culture.Name))
            {
                resCacheByCulture = _resourceCache[culture.Name];
                if (resCacheByCulture.ContainsKey(resourceKey))
                {
                    resourceValue = resCacheByCulture[resourceKey];
                }
            }

            // if not in the cache, go to the database
            if (resourceValue == null)
            {
                resourceValue = ResourceFacade.GetResourceByCultureAndKey(culture, resourceKey);

                // add this result to the cache
                // find the dictionary for this culture
                // add this key/value pair to the inner dictionary
                lock (this)
                {
                    if (resCacheByCulture == null)
                    {
                        resCacheByCulture = new Dictionary<stringstring>();
                        _resourceCache.Add(culture.Name, resCacheByCulture);
                    }
                    resCacheByCulture.Add(resourceKey, resourceValue);
                }
            }
            return resourceValue;
        }

        /// <summary>
        /// Returns a resource reader.
        /// </summary>
        public System.Resources.IResourceReader ResourceReader
        {
            get
            {
                Debug.WriteLine(String.Format(CultureInfo.InvariantCulture, "DBResourceProvider.get_ResourceReader - type:{0}"this.ClassKey));

                if (Disposed)
                {
                    throw new ObjectDisposedException("DBResourceProvider object is already disposed.");
                }

                // this is required for implicit resources 
                // this is also used for the expression editor sheet 

                ListDictionary resourceDictionary = ResourceFacade.GetResourcesByCulture(CultureInfo.InvariantCulture);

                return new DBResourceReader(resourceDictionary);
            }

        }

        #endregion

        protected override void Cleanup()
        {
            try
            {
                if (ResourceFacade != null)
                    GC.SuppressFinalize(ResourceFacade);
                this._resourceCache.Clear();
            }
            finally
            {
                base.Cleanup();
            }
        }
    }
+ DBResourceProviderFactory.cs 
    public class DBResourceProviderFactory : ResourceProviderFactory
    {

        public override IResourceProvider CreateGlobalResourceProvider(string classKey)
        {
            Debug.WriteLine(String.Format(CultureInfo.InvariantCulture, "DBResourceProviderFactory.CreateGlobalResourceProvider({0})", classKey));
            return new DBResourceProvider(classKey);
        }

        public override IResourceProvider CreateLocalResourceProvider(string virtualPath)
        {
            Debug.WriteLine(String.Format(CultureInfo.InvariantCulture, "DBResourceProviderFactory.CreateLocalResourceProvider({0}", virtualPath));

            // we should always get a path from the runtime
            string classKey = virtualPath;
            if (!string.IsNullOrEmpty(virtualPath))
            {
                //virtualPath = virtualPath.Remove(0, 1); // don't need it in ASP.NET MVC
                classKey = virtualPath.Remove(0, virtualPath.IndexOf('/') + 1);
            }

            return new DBResourceProvider(classKey);
        }
    }
 Now I build own Resource Factory for getting the Resource strings. This is a Class Diagram:
 
And the details of their implementation are:
+ IResourceFactory.cs
    public interface IResourceFactory
    {
        // Account controlelr
        string AdminAccountLogon_GetVirtualPath();
        string AdminAccountLogon_GetUserLoginString();
        string AdminAccountLogon_GetUserNameString();
        string AdminAccountLogon_GetPasswordString();
        string AdminAccountLogon_GetRememberMeString();
        // more some thing here
.....
    }
 + NMAResourceFactory.cs
    public partial class NMAResourceFactory : IResourceFactory
    {
        public NMAResourceFactory() : this(new HttpContextWrapper(HttpContext.Current))
        {
        }

        public NMAResourceFactory(HttpContextBase httpContext)
        {
            NMAContext = httpContext;
        }

        public HttpContextBase NMAContext { getprivate set; }
    }
+ My partial class:
    public partial class NMAResourceFactory
    {
        public string AdminAccountLogon_GetVirtualPath()
        {
            return "/Admin/Account/Index";
        }

        public string AdminAccountLogon_GetUserLoginString()
        {
            Check.Assert(NMAContext != null"NMAContext instance is null");

            return NMAContext.GetLocalResourceObject(AdminAccountLogon_GetVirtualPath(), "UserLogin").ToString();
        }

        public string AdminAccountLogon_GetUserNameString()
        {
            Check.Assert(NMAContext != null"NMAContext instance is null");

            return NMAContext.GetLocalResourceObject(AdminAccountLogon_GetVirtualPath(), "UserName").ToString();
        }

        public string AdminAccountLogon_GetPasswordString()
        {
            Check.Assert(NMAContext != null"NMAContext instance is null");

            return NMAContext.GetLocalResourceObject(AdminAccountLogon_GetVirtualPath(), "Password").ToString();
        }

        public string AdminAccountLogon_GetRememberMeString()
        {
            Check.Assert(NMAContext != null"NMAContext instance is null");

            return NMAContext.GetLocalResourceObject(AdminAccountLogon_GetVirtualPath(), "RememberMe").ToString();
        }
    }
 
Next, I put some custom configuration in Web.config:
  <system.web>
     ....................................
     <globalization culture="en-GB" uiCulture="en-GB" resourceProviderFactoryType="NMA.Web.Core.Provider.Resources.DBResourceProviderFactory, NMA.Web" />
  </system.web>
Now I want to use the new feature in .NET 4 is DataAnnotation for validating data for my ViewModel. 
I found the best article about Localisation for web form at 
http://adamyan.blogspot.com/2010/02/aspnet-mvc-2-localization-complete.html. 
But I could not use it with my DBResourceProvider, so that I must implement the cross cutting 
attribute that extend from DisplayNameAttribute's DataAnnotation. And the code as here:
    public class DBLocalizedDisplayNameAttribute : DisplayNameAttribute
    {
        private string _displayFunctionName;
        private Type _resourceFactory;

        public DBLocalizedDisplayNameAttribute(Type resourceFactory, string displayFunctionName)
       {
           _resourceFactory = resourceFactory;
           _displayFunctionName = displayFunctionName;
       }

        public override string DisplayName
        {
            get
            {
                Type ty = _resourceFactory;
                MethodInfo[] mi = ty.GetMethods();
                MethodInfo methodInfo = ty.GetMethod(_displayFunctionName);
                var o = Activator.CreateInstance(ty);
                var result = methodInfo.Invoke(o, null);
                return result.ToString();
            }
        }
    }
Finally I only need decorating my field in View Model as:
    public class LogOnModel
   {
        [Required]
        [DBLocalizedDisplayName(typeof(NMAResourceFactory), "AdminAccountLogon_GetUserNameString")]
        public string UserName { getset; }

        [Required]
        [DataType(DataType.Password)]
        [DBLocalizedDisplayName(typeof(NMAResourceFactory), "AdminAccountLogon_GetPasswordString")]
        public string Password { getset; }

        [DBLocalizedDisplayName(typeof(NMAResourceFactory), "AdminAccountLogon_GetRememberMeString")]
        public bool RememberMe { getset; }
    }
In the View, we only need using it normally as:
 
                <% using (Html.BeginForm("Index""Account"FormMethod.Post, new { id = "formLogin", @class = "loginform"})) { %>
                    <div class="login-body">
                        <fieldset>
                            <div class="messages" style="display:none">
                                <div class="error"><%= Html.ValidationSummary() %></div>
                            </div>
                            <dl>
                                <dt>                                    
                                    <%= Html.LabelFor(m => m.UserName) %>
                                </dt>
                                <dd>
                                    <%= Html.TextBoxFor(m => m.UserName, new { @Class = "inputtext", @size = "50" })%>                                    
                                </dd>
                            </dl>
                            <dl>
                                <dt>
                                    <%= Html.LabelFor(m => m.Password) %>
                                </dt>
                                <dd>
                                    <%= Html.PasswordFor(m => m.Password, new { @Class = "inputtext", @size = "50" })%>
                                    <%= Html.ValidationMessageFor(m => m.Password)%>
                                    <div>
                                        <%= Html.CheckBoxFor(m => m.RememberMe, new { Class = "checkbox floatleft" })%>
                                        <%= Html.LabelFor(m => m.RememberMe)%>
                                    </div>
                                    <br />
                                    <br />
                                </dd>
                            </dl>
                        </fieldset>
                        <fieldset>
                            <div class="floatleft" style="width80px;">
                                <div class="pributton left">
                                    <input id="btnLogin" name="btnLogin" type="submit" value="Login" class="rightbutton" /></div>
                            </div>
                        </fieldset>
                    </div>
                    <div class="login-footer">
                    </div>
                    <% } %>
And if you want to using it from ResourceFactory from your View, you might need declaration in your view header like that:
<% 
    NMA.Web.Core.ResourceFactory.IResourceFactory _resourceFactory = new NMA.Web.Core.ResourceFactory.NMAResourceFactory();
%>
call it as below:
                    <div class="login-header">
                        <h3><%= _resourceFactory.AdminAccountLogon_GetUserLoginString()%></h3>
                    </div>
 
That's all. You can find all source code in NMA project http://nma.codeplex.com. 
Hope this is useful for you. Reminder once again it is only my practice, 
maybe we have many best solution for implemented localisation.
Good bye and see you next time!

5 Comments

  • A simple question. If I want to replace the "hard coded" text int the statement [Required(ErrorMessage= "Error please ....." with a variable i.e. mytext; is this even possible? Do I have to recompile my program every time I need to change any error? This sound like a very bad idea!

    I do not want to use the resource file suggested by some people. I just want to have my own error messages file in a separate DB which I want to call with a web service and just replace the message with my own message coming from the outside world.

    Also, Do you have perhaps a more elavorated example that shows how to use the return base.DisplayName; ?

    How can I pass a variable to


    [

    Display(Name = "New password")]

    ===== > Display(Name = Mytext)]

    I do not want to hardcode the Display Name nor the Errormessage.

    We need this for a business app that is global in nature.

    Thanks a lot!

  • Oh! Awesome article! Thanks a lot ;)

  • Very nice... do you a source i can download>

  • Today first time i visited this blog and i am thrilled by getting this blog. Information is very effective and useful.

  • swvYMJ Hey, thanks for the blog article. Keep writing.

Comments have been disabled for this content.