In my project I'm doing a lot of native UI rendering1, soon I will blog on why.
Naturally I have many images to render using DrawImage methods, that requires an Image instance.
There are couple of ways to load an image but the best2 and easies option is using a Resource File, just dragging and dropping the images into the resource designer will add the file to the project and generate a nice static property by the name of the file in the static class generated by the name of the resource file.
The problem is that each time image resource property is called a new image instance is created with the requested image, so if the same image is used in multiple places3 it will be loaded multiple times in memory4.
The obvious solution is to cache the returned image and reuse the same instance.
Not dwell in the different considerations I needed the simplest cache that lazy loads the objects and hold them as long as the application is running. Using a simple dictionary is the obvious choice but it creates a lot of work around it when adding/removing the images used in the project, this is where T4 Templates come into play.
T4 Templates
From MSDN: In Visual Studio, a T4 text template is a mixture of text blocks and control logic that can generate a text file. The control logic is written as fragments of program code in Visual C# or Visual Basic. The generated file can be text of any kind, such as a Web page, or a resource file, or program source code in any language.
Cached Resources T4 Template
For my propose T4 Template is a script file that is located in the C# project next to the resource "Resources.resx" file and has the same name "Resources.tt" apart from the extension as it needs to be ".tt".
When the template is executed, either by context menu "Run Custom Tool" or saving the template file, it generate a new ".cs" file containing static "CachedResources" class with property for every image resource in the same way they are generated on .NET resource file, the difference is that on first call to the property it retrieves the image from the resource manager and caches it in a private static field of the class, all subsequent calls will return the cached instance.
This way the image is cached and not loaded multiple times and also there is no dictionary overhead as each cached image has its own cache field.
public static Bitmap SomeImage { get { if(_SomeImage == null) _UnreadIcon = (Bitmap)Resources.ResourceManager.GetObject("SomeImage"); return _SomeImage; } }
Template
<#@ template debug="false" hostspecific="true" language="C#" #> <#@ output extension=".cs" encoding="UTF8" #> <#@ assembly name="System.Core" #> <#@ assembly name="System.Xml" #> <#@ assembly name="System.Xml.Linq" #> <#@ import namespace="System.IO" #> <#@ import namespace="System.Xml" #> <#@ import namespace="System.Xml.Linq" #> <# // // Configurations: // ---- bool useCulture = false; string appName = "CachedResources"; string version = "1.0.1.0"; string accessibility = "public"; string ns = (string)System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("NamespaceHint"); string resxFileName = Path.ChangeExtension(Host.TemplateFile, ".resx"); string resxClassName = Path.GetFileNameWithoutExtension(Host.TemplateFile); string proxyClassName = string.Format("Cached{0}", resxClassName); XDocument document = XDocument.Parse(File.ReadAllText(resxFileName)); // DO NOT CHANGE THE TEMPLATE UNDER THIS LINE!! #>// "Therefore those skilled at the unorthodox // are infinite as heaven and earth, // inexhaustible as the great rivers. // When they come to an end, // they bagin again, // like the days and months; // they die and are reborn, // like the four seasons." // // - Sun Tsu, // "The Art of War" // --- // This code was generated by Resource extnsions template for T4 C# // // DO NOT CHANGE THIS FILE! //Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. using System.Drawing; namespace <#=ns#> { // ReSharper disable InconsistentNaming // ReSharper disable ResourceItemNotResolved /// <summary> /// Represent a cached resources class for "<#= resxClassName #>" resources. /// </summary> [System.Diagnostics.DebuggerStepThroughAttribute] [System.CodeDom.Compiler.GeneratedCode("<#= appName #>", "<#= version #>")] <#= accessibility #> static class <#= proxyClassName #> { #region Fields and Consts <# foreach(var item in document.Element("root").Elements("data")) { if (IsHandled(item)) { #> private static <#= GetType(item) #> <#= GetFieldName(item) #>; <# } } #> #endregion <# foreach(var item in document.Element("root").Elements("data")) { if (IsHandled(item)) { #> /// <summary> /// Get the cached bitmap image of "<#= item.Attribute("name").Value #>" resource.<br/> /// On first call the image is cached, all subsequent calls will return the same image object. /// </summary> public static <#= GetType(item) #> <#= GetPropName(item) #> { get { if(<#= GetFieldName(item) #> == null) <#= GetFieldName(item) #> = (<#= GetType(item) #>)<#= resxClassName #>.ResourceManager.GetObject("<#= GetName(item) #>"<#if(useCulture) {#>, <#= resxClassName #>.Culture <#}#>); return <#= GetFieldName(item) #>; } } <# } } #> /// <summary> /// Clear all cached images. /// </summary> public static void ClearCache() { <# foreach(var item in document.Element("root").Elements("data")) { if (IsHandled(item)) { #> var <#= GetName(item) #>2 = <#= GetFieldName(item) #>; <#= GetFieldName(item) #> = null; if( <#= GetName(item) #>2 != null) <#= GetName(item) #>2.Dispose(); <# } } #> } } // ReSharper restore ResourceItemNotResolved // ReSharper restore InconsistentNaming } <#+ public bool IsHandled(XElement item) { return item.Value.Contains("System.Drawing.Bitmap") || item.Value.Contains("System.Drawing.Icon"); } public string GetType(XElement item) { if(item.Value.Contains("System.Drawing.Bitmap")) return "Bitmap"; if(item.Value.Contains("System.Drawing.Icon")) return "Icon"; return "Unknown"; } public string GetName(XElement item) { return item.Attribute("name").Value; } public string GetPropName(XElement item) { var str = item.Attribute("name").Value; return char.ToUpper(str[0]) + str.Substring(1); } public string GetFieldName(XElement item) { return "_" + item.Attribute("name").Value; } #>
-
Overwriting the OnPaint method of a control and doing all the UI rendering manually using the past Graphics instance.
↩ -
Loading image from stream requires the stream to remain open during the use of the image or it is possible that for some images the DrawImage method will throw generic GDI exception.
↩ -
Also because Image object has a Finalize method it can remain in memory for quite some time.
↩ - for reasons beyond my understanding the memory footprint of an image is 7-9 times the size of the image on disk so it can significant. ↩
[…] itself this may look futile as there is no way to lower this overhead**, but as I have explained in my previous post the most common use of images in .NET GUI applications is via resource files (controls use same […]
[…] can get the template and read all about it in my “Using T4 Templates for caching image resources” […]
[…] itself this may look futile as there is no way to lower this overhead2, but as I have explained in my previous post the most common use of images in .NET GUI applications is via resource files (controls use same […]
[…] request will return the same 'Image' instance. My proposal is to use 'Resources.resx' to add the images to the project and then create 'CachedResources' static class […]