Dotneteers.net
All for .net, .net for all!

LVN! Sidebar #2 - Resolving string resources

 

If you used the VSPackage wizard—and I am sure you used it by now—you can discover that it creates a VSPackage.resx file and also a Resources.resx file to store localizable resource information. Attributes used for package registration have indirect references to strings in the VSPackage.resx file. A good example is the InstalledProductRegistration attribute:

[InstalledProductRegistration(false, "#110", "#112", "1.0", IconResourceID = 400)]

public selaed class MyPackage: Package { ... }

The highlighted parameters of the attribute are indirect references to the strings with the ID of 110 and 112 in the VSPackage.resx file. I like this mechanism, so I decided to create a similar behavior to use it in my packages. In this blog post I will show you how I have done it.

Referencing string resources

We can put resources either into the VSPackage.resx or to the Resources.resx file (and even to other .resx files). By the way, why to have two .resx files? In the current implementation of VS SDK, the infrastructure in Visual Studio Shell (do not forget it is based on COM technology!) can co-operate with resources in the VSPackage.resx file. The build targets used by VSPackages embed the resources in VSPackage.resx file so that Visual Studio Shell can access them. Sorry, but in this blog post I am not going to explain how it is done...

We like Resources.resx file since the ResXFileCodeGenerator custom tool attached to the file generates code where resources can be accessed through properties.

I decided to create a string resolution mechanism where you can resolve strings in localizable resources either declared in VSPackage.resx or in Resources.resx. I have created a generic class called StringResolver having the following blueprint:

public static class StringResolver<TPackage>

  where TPackage: Package

{

  public static string Resolve(string toResolve);

  public static bool ExistsInResourcesClass(string key);

  public static string ResolveInResourcesClass(string key);

  public static bool ExistsInPackageResources(string key);

  public static string ResolveInPackageResources(string key);

  public static Type GetResourcesClassType();

}

The Resolve method is intended to be used most of the time. This method accepts a string parameter and returns with a string resolved by the input parameter. If the input string starts with “#”, the trailing part is treated as a string resource key in VSPackages.resx. Should the input string begin with “$”, the trailing part is handled as a property name in the Resources class corresponding to the class generated by the ResXFileCodeGenerator custom tool from the Resources.resx file. In any other cases the method retrieves the input string.

I provided the ResolveInResourcesClass and ResolveInPackageResources static helper methods to search for resource strings within target locations referenced in method names. The ExistsInResourcesClass and ExistsInPackageResources methods are provided for pre-checking resources before accessing them.

Implementation details

The StringResolver class expects a type parameter of TPackage that must be an inheritor of the Package class. Why is it so? To find package resources we have to use the IVsResourceManager (interop) interface to call its LoadResourceString method where we have to pass the GUID of the current package as an input parameter. If we had only one package class in our package assembly (as generally we have) we could scan the types with Reflection to find the package class and take its GUID. However, it is allowed to put more than one package into the assembly and in this case scanning through types would not work.

Actually I use scanning through types to obtain the Resources class (generated by ResXFileCodeGenerator). The class is used when we want to resolve a string resource within Resources.resx. It is not a resource-saving behavior to scan for the Resources class every time we have to resolve a string, so I decided to add a caching mechanism to StringResolver.

Finding the Resources class

StringResolver is in a separate utility class library (VsxLibrary). When we create one or more packages using VsxLibrary, each package intends to use Resource.resx in its own assembly. For that reason we address the corresponding Resources classes through the containing assembly. Here is the code for the GetResourcesClassType method that obtains the appropriate Resources class for us:

public static class StringResolver<TPackage>

  where TPackage: Package

{

  private static readonly Dictionary<Assembly, Type> _ResourceTypes =

      new Dictionary<Assembly, Type>();

  // ...

 

  public static Type GetResourcesClassType()

  {

    Assembly callingAsm = typeof(TPackage).Assembly;

 

    // --- Check the cache for Resources type and return if found.

    Type resourceType;

    if (_ResourceTypes.TryGetValue(callingAsm, out resourceType))

    {

      return resourceType;

    }

 

    // --- Search for the Resources type. If found add it to the cache.

    foreach (Type type in callingAsm.GetTypes())

    {

      if (type.GetCustomAttributes(typeof(CompilerGeneratedAttribute),

        false).Length > 0 && !type.IsVisible && type.Name == "Resources")

      {

        _ResourceTypes.Add(callingAsm, type);

        return type;

      }

    }

    return null;

  }

  // ...

}

 

Instead of every time scanning for the Resources class in the package assembly, we store them in a cache addressed by the package assembly itself. If we do not find Resources class for the assembly in the cache, we scan through the compiler generated classes with the name of “Resources” in the assembly and retrieve the first we found.

Obtaining resource properties

Having the Resources class it is quite easy either to obtain the resource by the corresponding property name or check if the property exists. This is the responsibility of ResolveInResourcesClass and ExistsInResourcesClass methods:

// ...

public static bool ExistsInResourcesClass(string key)

{

  Type resourceType = GetResourcesClassType();

  if (resourceType == null) return false;

  PropertyInfo propInfo = resourceType.GetProperty(key, BindingFlags.Static |

    BindingFlags.NonPublic);

  return propInfo != null;

}

// ...

public static string ResolveInResourcesClass(string key)

{

  Type resourceType = GetResourcesClassType();

  if (resourceType == null) return Resources.ResourceNotFound;

  PropertyInfo propInfo =

    resourceType.GetProperty(key, BindingFlags.Static | BindingFlags.NonPublic);

  if (propInfo == null) return Resources.ResourceNotFound;

  return propInfo.GetValue(null, null).ToString();

}

// ...

Obtaining package resources

In Visual Studio Packages the SVsResourceManager service is the one that provides methods to access package resources through its IVsResourceManager interface. We use only the LoadResourceString method. Using this interface is quite straightforward:

// ...

public static bool ExistsInPackageResources(string key)

{

  Guid packageGuid = GetPackageGuid();

  string resourceString;

  IVsResourceManager resourceManager =

    (IVsResourceManager)Package.GetGlobalService(typeof(SVsResourceManager));

  if (resourceManager == null) return false;

  int result = resourceManager.LoadResourceString(ref packageGuid, -1, key,

    out resourceString);

  return result == VSConstants.S_OK;

}

// ...

public static string ResolveInPackageResources(string key)

{

  Guid packageGuid = GetPackageGuid();

  string resourceString;

  IVsResourceManager resourceManager =

    (IVsResourceManager) Package.GetGlobalService(typeof (SVsResourceManager));

  if (resourceManager == null) return Resources.PackageNotFound;

  int result = resourceManager.LoadResourceString(ref packageGuid, -1, key,

    out resourceString);

  if (result != VSConstants.S_OK) return Resources.PackageNotFound;

  return resourceString;

}

// ...

private static Guid GetPackageGuid()

{

  return typeof(TPackage).GUID;

}

// ...

Putting the pieces together

Now, we have every piece to create the Resolve method:

public static string Resolve(string toResolve)

{

  if (string.IsNullOrEmpty(toResolve) || toResolve.Length < 2) return toResolve;

  if (toResolve.StartsWith("#"))

  {

    toResolve = toResolve.Substring(1);

    return toResolve.StartsWith("#")

      ? toResolve : ResolveInPackageResources(toResolve.Trim());

  }

  else if (toResolve.StartsWith("$"))

  {

    toResolve = toResolve.Substring(1);

    return toResolve.StartsWith("$")

      ? toResolve : ResolveInResourcesClass(toResolve.Trim());

  }

  return toResolve;

}

As you see from the code we can use “##” and “$$” if we would like the first “#” or “$” taken into account as literals instead of resolver marks.

Using StringResolver

When you create a package, it is quite easy to use StringResolver.  Here I do not show you a full source code but only a small code snippet good enough to imagine how to resolve strings:

public selaed class MyPackage: Package

{

  // ...

  protected override void Initialize()

  {

    // ...

    string myWindowCaption = StringResolver<MyPackage>.Resolve("$WindowTitle");

    // ...

  }

  // ...

}

In the next LVN! Sidebar I show you some more examples of using StringResolver.


Posted Mar 24 2008, 08:12 AM by inovak
Filed under: ,

Add a Comment

(required)  
(optional)
(required)  
Remember Me?