Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
Markdown Monster - The Markdown Editor for Windows

Custom Localization ResourceProviders and Compiler Errors


:P
On this page:

I’ve been struggling with building a custom ResourceManager and hooking it into ASP.NET over the last few days as part of a localization engine that is data driven rather than through Resources. I’ve had a separate engine for this for sometime, but it hasn’t been hooked into the .NET or ASP.NET infrastructure so went back and tried to figure out how to build a custom resource manager and resource provider to hook it into ASP.NET.

 

This has been a pretty painful experience for me. The ResourceManager class is not well setup for extensibility – no interfaces, you have to basically inherit and selectively override methods in various classes (ResourceManager, ResourceSet, ResourceReader, ResourceWriter). With the help of some code I found I was able to make that work. The ASP.NET interfaces ResourceProvider and ResourceProviderFactory are a little better, but not much – also no interfaces but at least these interfaces are relatively simple and it’s easy to see what needs to be overridden. So after a painful few days I managed to get my code hooked up and running and serving resources.

 

It works in the development project and I can run the page just fine if I use in place compilation that VS.NET uses in development. However, when I compile the project I get an error like this:

:

 

The resource object with key 'LinkButton1Resource1.Text' was not found.
error ASPPARSE: /LocalizationTest.aspx(Line 27)

 

LinkButton1 looks something like this:

 

<asp:LinkButton ID="LinkButton1" runat="server"  Text="<%$ Resources:LinkButton1Resource1.Text %>" meta:resourcekey="LinkButton1Resource2"></asp:LinkButton>

 

Which is an explicit expression and as I mentioned this works fine in dev mode. At pre-compile time however this fails outright.

 

ASP.NET compiles pages and does something like this for the parse tree:

 

private LinkButton __BuildControlLinkButton1()

{

      LinkButton button1 = new LinkButton();

      base.LinkButton1 = button1;

      button1.ApplyStyleSheetSkin(this);

      button1.ID = "LinkButton1";

      button1.Text = Convert.ToString(base.GetLocalResourceObject("LinkButton1Resource1.Text"), CultureInfo.CurrentCulture);

      return button1;

}

 

Notice that it assigns the resource directly by calling the base.GetLocalResourceObject, which in turn calls the local resource manager. That’s reasonable, but it doesn’t explain WHY the compiler would need to have access to this resource.

 

So I took out my custom provider and compiled the Resource based version. Now it too will fail if there’s a missing explicit or implicit resource in the base (Invariant) resource file. Apparently ASP.NET tries to be overly helpful by checking and making sure the resource exists. This must be some sort of explicit check though because nothing that gets compiled into the page really changes in this respect. This is a compiler issue.

 

 

So now that I sort of understand how this works I can see that the ResourceManager needs to be accessible and working at Compile time. How freaking LAME is that? That pretty much defeats any dynamic mechanism for loading resources at runtime.

 

Ok, fine, to see if I could get this to work I hardcoded my connection string into the ResourceManager (not realistic I know for a real application but I’ll worry about that later) and then hooked it back up and re-ran the compile. I also hooked up SQL Profiler to see if it’s hitting the database and what it’s asking for. Sure enough the database gets hit and the compiler is asking for the en-us and en ResourceSets which should work fine. Both of these include the resource that the precompiler is failing on. In theory at least my provider should be feeding the appropriate resource set now and the compiler should be finding the correct resource string.

 

But it’s still failing…

 

This is looking more and more like I wasted 2 days working on this because of a bad compiler check implementation. The code runs just fine, but the damn compiler isn’t happy because it find resources at compile time.

 

I guess one solution to this debacle is to use Web Application Projects which doesn’t pre-compile the ASPX pages and defers this process until runtime where it appears to work correctly. But then using Web Deployment Projects on the project won’t work…

 

Has anybody been down this path and figured out a way around this mess?

Posted in ASP.NET  Localization  

The Voices of Reason


 

Rick Strahl
October 04, 2006

# re: Custom Localization ResourceProviders and Compiler Errors

Ok... here's a real hack solution to this problem:

I can override The IResourceProvider.GetObject method and catch any NULL returns and make it return an empty value instead. Now the app compiles properly:

object IResourceProvider.GetObject
(string resourceKey, CultureInfo culture)
{
if (culture == null || culture == CultureInfo.InvariantCulture)
culture = CultureInfo.CurrentUICulture;

object value = this.ResourceManager.GetObject(resourceKey,culture);

if (value == null)
return "";

return value;
}

Now this works, but it's a problem for any non-string resources unfortunately, so I'd need to check for specific type information here.

Eilon (Microsoft)
October 04, 2006

# re: Custom Localization ResourceProviders and Compiler Errors

Hi Rick,
ASP.NET checks that a *neutral* resource exists. It doesn't care whether you have a localized resource for a given property. It does this just to make sure you have no typos. I can see how it could be frustrating, but we felt this was a lesser evil.

Also if you'd like to see a sample localization provider I wrote, there's on here: http://www.leftslipper.com/ShowFaq.aspx?FaqId=9

Thanks,
Eilon

PS: I'm having trouble with your CAPTCHA. It took me about 4 refreshes to get it to display. It kept rendering as an invalid image.

David Ebbo
October 04, 2006

# re: Custom Localization ResourceProviders and Compiler Errors

Rick, please note that your statement above that "nothing that gets compiled into the page really changes in this respect" is not quite correct. If ASP.NET finds that a specific resource entry doesn't exist at compile time, it does not generate the call to GetLocalResourceObject() which you show above.

The reason for this is basically an optimization: controls have many properties, and it would be very expensive to attempt calling GetLocalResourceObject() for each of them just in case it exists at runtime. So we took the approach that to use localization for a specific attribute, you need to 'declare' your intent by returning something at compile time.

I grant you that this makes it somewhat less dynamic, but that was the price to pay to get acceptable perf.

Rick Strahl
October 04, 2006

# re: Custom Localization ResourceProviders and Compiler Errors

Eilon, yes I started down another path by just implementing a resource provider and skipping the Resource Manager which makes life a lot easier to see what's going on. I'll have to take a look at your's - I found another on CodeProject here:

http://www.codeproject.com/aspnet/customsqlserverprovider.asp?msg=1674170

and I see one of the things happening there is to also autofill a value if none is found.

I'm not sure how safe that is to automatically try and 'make up' a blank value though (don't have it running just yet <s>) because I think that might cause some problems with Resource fallback.

Also, I'm not clear on what order these calls are fired - it looks (from what I've seen so far) that the invariant resource is called first and the the culture specific ones after that. Hmmm... it might be the compilation step that's actually doing the Invariant calls. I'll have to take a closer look once I get this new simpler provider running - it'll be easier to see what's going on with that.

David, I think if a specific resource doesn't exist the ASP.NET compiler fails altogether and won't compile the page. (I get a parser error: The resource object with key 'LinkButton1Resource1.Text' was not found in the page in an inplace page).

David Ebbo
October 04, 2006

# re: Custom Localization ResourceProviders and Compiler Errors

Yes, you are right in the explicit case. I was refering to the implicit local resource case. e.g. suppose you have:
<asp:Label runat="server" meta:resourcekey="Label1Resource1"/>

Here, you will get different code generated based on what the resource provide returns at compile time. e.g. it will generate a GetLocalResourceObject() assignment for teh Text property if and on if the provider returns non null for the key "Label1Resource1.Text".

Andrey
October 05, 2006

# re: Custom Localization ResourceProviders and Compiler Errors

If you want to check whether your resource was compiled into the assembly or no, please use MSIL disasembler
D:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\ildasm.exe

There you can see all the resources and the most important , the namespace for resources. As far as i remember all issues i had with resources was in wrong namespace, because when you getting any resource, you getting it with namespace+name. Check it please.

Vadi
October 05, 2006

# re: Custom Localization ResourceProviders and Compiler Errors

I came across one Localization scenario where in I need to display some text thru Javascript, All the localized string values are stored in the server thru the .resource files and it is something like "Loading.." kind of text I wanted to display in the page.

I was wondering how to make use Server Resource to feed the resource texts to JS scripts or is there any other hack ??

I guess, anybody out here can understand this problem ?

Rick Strahl
October 06, 2006

# Trying to implement IImplicitResourceProvider - Rick Strahl

I'm still trying to implement a custom Localization provider and while I've made the base functionality work fairly easily I'm stuck on the IImplicitResourceProvider interface not being quite sure where it needs to go and what it EXACTLY needs to do. I have it firing but it's not working correctly...

Rob Thijssen
May 15, 2007

# re: Custom Localization ResourceProviders and Compiler Errors

I have begun a project to implement Custom Resource providers for a variety of resource repositories and am currently working to make my DesignTimeResourceProvider work. Specifically, I want Visual Studio's 'Generate Local Resource' tool to be able to create the resources in my custom provider as seamlessly as it already generates resx files. The project is still in it's infancy but currently supports using SQL Server and Stored Procedures as a resource repository. My code is GPL'ed and available for download at: http://code.google.com/p/localisationprovider/

Grady
September 18, 2007

# re: Custom Localization ResourceProviders and Compiler Errors

I have successfully created a custom resource provider to support custom xml files, but I am getting errors when trying to compile the project. If I view the page, I don't get any errors. The errors only occur during compliation. Below is my code. Do you have any suggestions on how to make the compiler errors go away? Thanks!!

using System;
using System.Web.Compilation;
using System.Globalization;
using System.Resources;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Xml;
using System.Web;

namespace CreativePlug.Providers
{
    public sealed class XmlResourceProviderFactory : ResourceProviderFactory
    {

        public XmlResourceProviderFactory()
        {
        }

        public override IResourceProvider CreateGlobalResourceProvider(string classKey)
        {
            return new XmlResourceProvider(null, classKey);
        }

        public override IResourceProvider CreateLocalResourceProvider(string virtualPath)
        {
            virtualPath = this.GetVirtualPath(virtualPath);
            return new XmlResourceProvider(virtualPath, null);
        }

        public class XmlResourceProvider : IResourceProvider
        {
            private string _virtualPath;
            private string _className;
            private IDictionary _resource;

            public XmlResourceProvider(string virtualPath, string className)
            {
                _virtualPath = virtualPath;
                _className = className;
            }

            private IDictionary GetResource(string cultureName)
            {
                _resource = new ListDictionary();

                IDictionary resourceDict = _resource as IDictionary;

                resourceDict = XmlResourceHelper.GetResources(_virtualPath, _className, cultureName, false, null);
                _resource = resourceDict;

                return resourceDict;
            }

            public object GetObject(string resourceKey, CultureInfo culture)
            {
                if (culture == null || culture == CultureInfo.InvariantCulture)
                    culture = CultureInfo.CurrentUICulture;

                object value = GetResource(culture.Name)[resourceKey];

                return value;
            }

            public System.Resources.IResourceReader ResourceReader
            {
                get
                {
                    return new XmlResourceReader(GetResource(CultureInfo.CurrentUICulture.Name));
                }
            }
        }

        private sealed class XmlResourceReader : IResourceReader
        {
            private IDictionary _resources;

            public XmlResourceReader(IDictionary resources)
            {
                _resources = resources;
            }

            IDictionaryEnumerator IResourceReader.GetEnumerator()
            {
                return _resources.GetEnumerator();
            }

            void IResourceReader.Close()
            {
            }

            IEnumerator IEnumerable.GetEnumerator()
            {
                return _resources.GetEnumerator();
            }

            void IDisposable.Dispose()
            {
            }
        }

        internal static class XmlResourceHelper
        {
            public static IDictionary GetResources(string virtualPath, string className, string cultureName, bool designMode, IServiceProvider serviceProvider)
            {
                ListDictionary resources = new ListDictionary();

                if (!String.IsNullOrEmpty(virtualPath))
                {
                    // Get Local resources
                    XmlDocument configurationFile = new XmlDocument();
                    string xmlFilePath = HttpContext.Current.Server.MapPath(HttpContext.Current.Request.ApplicationPath + "/LocalResources/" + virtualPath + "_" + cultureName + ".xml");
                    configurationFile.Load(xmlFilePath);
                    XmlNodeList parentNodeList = configurationFile.SelectNodes("Resource/item");
                    foreach (XmlNode childNode in parentNodeList)
                    {
                        string rn = childNode.Attributes.Item(0).Value.ToString();
                        string rv = childNode.InnerText;
                        resources.Add(rn, rv);
                    }
                }
                else if (!String.IsNullOrEmpty(className))
                {
                    // Get Global resources
                    XmlDocument configurationFile = new XmlDocument();
                    string xmlFilePath = HttpContext.Current.Server.MapPath(HttpContext.Current.Request.ApplicationPath + "/GlobalResources/Global." + cultureName + ".xml");
                    configurationFile.Load(xmlFilePath);
                    XmlNodeList parentNodeList = configurationFile.SelectNodes("Resource/item");
                    foreach (XmlNode childNode in parentNodeList)
                    {
                        string rn = childNode.Attributes.Item(0).Value.ToString();
                        string rv = childNode.InnerText;
                        resources.Add(rn, rv);
                    }
                }

                return resources;
            }

        }

        private string GetVirtualPath(string virtualPath)
        {
            string virtualFileName = System.IO.Path.GetFileName(virtualPath);
            string virtualFileNameWithoutExtension = System.IO.Path.GetFileNameWithoutExtension(virtualPath);
            virtualPath = virtualPath.Remove(0, 1);
            virtualPath = virtualPath.Replace(HttpContext.Current.Request.ApplicationPath, "");
            virtualPath = virtualPath.Replace("/", "_");
            virtualPath = virtualPath.Replace(virtualFileName, virtualFileNameWithoutExtension);
            virtualPath = virtualPath.Replace("Localization_", "");

            return virtualPath;
        }
    }
}

Rick Strahl
September 18, 2007

# re: Custom Localization ResourceProviders and Compiler Errors

My guess would be that HttpContext may not be available during compilation. Since the compiler generates code for implicit and explicit resource expressions and DOESN'T generate them if they are missing that's likely what you're seeing.

Grady
September 18, 2007

# re: Custom Localization ResourceProviders and Compiler Errors

Do you have any suggestions for a solution? FYI the error I get is "object reference not set to an instance of an object" for all controls that have a "meta:resourceKey" tag. Thanks!

Rick Strahl
September 18, 2007

# re: Custom Localization ResourceProviders and Compiler Errors

I can't remember the details, but if you debug you should see that HttpContext.Current is probably null. At that point you need to figure out the path some other way.

I think if I remember right you can get at the path using the VirtualPathProvider even during compilation. Take a look at the ImplicitResourceProvider for the design time implementation - it does something similar to figure the base paths for the application.

Grady
September 19, 2007

# re: Custom Localization ResourceProviders and Compiler Errors

Now I seem to be getting the same error that you were getting when you wrote this article. I get an the following error when using this caption="<%$ Resources:Global, Caption %>" on a gridview. I am using the following in the GetObject method but I continue to get the following error when I complile "The resource object with key 'Caption' was not found". The page will run fine, I only get the error when I compile. Did you ever figure this out?

object IResourceProvider.GetObject(string resourceKey, CultureInfo culture)
{
    if (culture == null || culture == CultureInfo.InvariantCulture)
        culture = CultureInfo.CurrentUICulture;

    object value = GetResource(culture.Name)[resourceKey];

    if (value == null)
        return "";

    return value;
}


FYI, the following worked for me...

Replaced...
HttpContext.Current.Server.MapPath
with...
System.Web.Hosting.HostingEnvironment.MapPath

Replaced...
HttpContext.Current.Request.ApplicationPath
with....
System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath

Praveen K Prasad
November 17, 2007

# re: Custom Localization ResourceProviders and Compiler Errors

I ran into the same problem but Scott Hanselman saved my A$$.
Here is the link
http://www.hanselman.com/blog/AmIRunningInDesignMode.aspx

shailesh patel
April 14, 2009

# re: Custom Localization ResourceProviders and Compiler Errors

Rick, I am having the same compilation issue. I modified GetObject method to return null eventhough having the same issue.

Let me tell you what I did so far. I am using custom resource provider and my resource files(plain .resx file not a satellite assembly) are residing somewhere in file system(so that other application share this .resx file). If I ran the web application, It works fine without any run time error but when I try to build the application it gives me an compliation error "resource object with key 'key' was not found'. You have ran into the same error so probably you can easily give me some idea what could be the wrong.

I have spent couple days but not able to fix this error.

Thanks
S.

Piotr Jaworski
June 10, 2013

# re: Custom Localization ResourceProviders and Compiler Errors

object IResourceProvider.GetObject(string resourceKey, CultureInfo culture)
{
if (AspHelpers.ASPNetCompilerExecutionContext)
return "ASPNetCompilerDesignTimeExecutionContext";


----------

where:


public static bool ASPNetCompilerExecutionContext
{
get
{
string entryMethod = (new StackTrace()).GetFrame((new StackTrace()).FrameCount - 1).GetMethod().Name;
if (entryMethod == "PrecompileApp")
return true;
else
return false;
}
}

Paresh
April 15, 2014

# re: Custom Localization ResourceProviders and Compiler Errors

Hi,
I am trying sample code available at http://www.west-wind.com/WestwindWebToolkit/Westwind.GlobalizationWeb/default.aspx
When i implement same in my web site, it gives "object reference not set to an instance of an object" at the time of compilation.
If i host sample code in IIS, it also start giving same error.

Can anyone please suggest solution for it?
Thanks,

Rick Strahl
April 15, 2014

# re: Custom Localization ResourceProviders and Compiler Errors

I recommend you look at the updated version of this provider at:

http://west-wind.com/Westwind.Globalization/

Much improved and less configuration you have to deal with - same tool, just a newer version and easier integration.

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024