C#: String.Inject() - Format strings by key tokens
I generally prefer to use String.Format() instead of doing a bunch of string additions, but constantly find myself splitting the code onto multiple lines with an appended comment to keep track of the indices:
string myString = string.Format("{0} is {1} and {2} is {3}",
o.foo, //0
o.bar, //1
o.yadi, //2
o.yada //3
);
Wouldn't it be nice you could instead write something like this?:
string myString = "{foo} is {bar} and {yadi} is {yada}".Inject(o);
Well, now you can - see the string extension method Inject below; it accepts an object, IDictionary or HashTable and replaces the property name/key tokens with the instance values. Since it uses string.Format internally, it also supports string.Format-like custom formatting:
1: using System;
2: using System.Text.RegularExpressions;
3: using System.Collections;
4: using System.Globalization;
5: using System.ComponentModel;
6:
7: [assembly: CLSCompliant(true)]
8: namespace StringInject
9: {
10: public static class StringInjectExtension
11: {
12: /// <summary>
13: /// Extension method that replaces keys in a string with the values of matching object properties.
14: /// <remarks>Uses <see cref="String.Format()"/> internally; custom formats should match those used for that method.</remarks>
15: /// </summary>
16: /// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param>
17: /// <param name="injectionObject">The object whose properties should be injected in the string</param>
18: /// <returns>A version of the formatString string with keys replaced by (formatted) key values.</returns>
19: public static string Inject(this string formatString, object injectionObject)
20: {
21: return formatString.Inject(GetPropertyHash(injectionObject));
22: }
23:
24: /// <summary>
25: /// Extension method that replaces keys in a string with the values of matching dictionary entries.
26: /// <remarks>Uses <see cref="String.Format()"/> internally; custom formats should match those used for that method.</remarks>
27: /// </summary>
28: /// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param>
29: /// <param name="dictionary">An <see cref="IDictionary"/> with keys and values to inject into the string</param>
30: /// <returns>A version of the formatString string with dictionary keys replaced by (formatted) key values.</returns>
31: public static string Inject(this string formatString, IDictionary dictionary)
32: {
33: return formatString.Inject(new Hashtable(dictionary));
34: }
35:
36: /// <summary>
37: /// Extension method that replaces keys in a string with the values of matching hashtable entries.
38: /// <remarks>Uses <see cref="String.Format()"/> internally; custom formats should match those used for that method.</remarks>
39: /// </summary>
40: /// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param>
41: /// <param name="attributes">A <see cref="Hashtable"/> with keys and values to inject into the string</param>
42: /// <returns>A version of the formatString string with hastable keys replaced by (formatted) key values.</returns>
43: public static string Inject(this string formatString, Hashtable attributes)
44: {
45: string result = formatString;
46: if (attributes == null || formatString == null)
47: return result;
48:
49: foreach (string attributeKey in attributes.Keys)
50: {
51: result = result.InjectSingleValue(attributeKey, attributes[attributeKey]);
52: }
53: return result;
54: }
55:
56: /// <summary>
57: /// Replaces all instances of a 'key' (e.g. {foo} or {foo:SomeFormat}) in a string with an optionally formatted value, and returns the result.
58: /// </summary>
59: /// <param name="formatString">The string containing the key; unformatted ({foo}), or formatted ({foo:SomeFormat})</param>
60: /// <param name="key">The key name (foo)</param>
61: /// <param name="replacementValue">The replacement value; if null is replaced with an empty string</param>
62: /// <returns>The input string with any instances of the key replaced with the replacement value</returns>
63: public static string InjectSingleValue(this string formatString, string key, object replacementValue)
64: {
65: string result = formatString;
66: //regex replacement of key with value, where the generic key format is:
67: //Regex foo = new Regex("{(foo)(?:}|(?::(.[^}]*)}))");
68: Regex attributeRegex = new Regex("{(" + key + ")(?:}|(?::(.[^}]*)}))"); //for key = foo, matches {foo} and {foo:SomeFormat}
69:
70: //loop through matches, since each key may be used more than once (and with a different format string)
71: foreach (Match m in attributeRegex.Matches(formatString))
72: {
73: string replacement = m.ToString();
74: if (m.Groups[2].Length > 0) //matched {foo:SomeFormat}
75: {
76: //do a double string.Format - first to build the proper format string, and then to format the replacement value
77: string attributeFormatString = string.Format(CultureInfo.InvariantCulture, "{{0:{0}}}", m.Groups[2]);
78: replacement = string.Format(CultureInfo.CurrentCulture, attributeFormatString, replacementValue);
79: }
80: else //matched {foo}
81: {
82: replacement = (replacementValue ?? string.Empty).ToString();
83: }
84: //perform replacements, one match at a time
85: result = result.Replace(m.ToString(), replacement); //attributeRegex.Replace(result, replacement, 1);
86: }
87: return result;
88:
89: }
90:
91:
92: /// <summary>
93: /// Creates a HashTable based on current object state.
94: /// <remarks>Copied from the MVCToolkit HtmlExtensionUtility class</remarks>
95: /// </summary>
96: /// <param name="properties">The object from which to get the properties</param>
97: /// <returns>A <see cref="Hashtable"/> containing the object instance's property names and their values</returns>
98: private static Hashtable GetPropertyHash(object properties)
99: {
100: Hashtable values = null;
101: if (properties != null)
102: {
103: values = new Hashtable();
104: PropertyDescriptorCollection props = TypeDescriptor.GetProperties(properties);
105: foreach (PropertyDescriptor prop in props)
106: {
107: values.Add(prop.Name, prop.GetValue(properties));
108: }
109: }
110: return values;
111: }
112:
113: }
114: }
File downloads:
Labels: asp.net, c#, experiment, howto, moss, programming, sharepoint, tools, visual studio, windows forms
10 Comments:
That's really nice work... I grumble about that kind of thing all the time.
By
Anonymous, at Thursday, August 07, 2008 6:04:00 PM
JDP, thanks. I anticipate using it, or an extension of the extension for some upcoming SharePoint work.
I believe the regular expression could be made more readable, and perhaps xslt would be a better parsing mechanism for large strings, but all in all I am happy with this.
By
Oskar Austegard, at Friday, August 08, 2008 8:11:00 AM
This post has now been haacked. I'll have to make some changes and repost at a later date...
By
Oskar Austegard, at Monday, January 05, 2009 10:29:00 AM
I like this, though ideally I would suggest doing this slightly differently by extending the string class to get string.Inject(object, string) and open up for intellisense since the context would be already set with the first object parameter.
By
Anonymous, at Tuesday, January 06, 2009 5:13:00 AM
@danijels, this IS a string extension. Not sure if you're misunderstanding me, or I'm misunderstanding you...
By
Oskar Austegard, at Tuesday, January 06, 2009 9:06:00 AM
haacked again... Also see Peli's Farm: Named Formats Pex Testimonium
By
Oskar Austegard, at Tuesday, February 03, 2009 5:09:00 PM
First of all. Thanks very much for your useful post.
I just came across your blog and wanted to drop you a note telling you how impressed I was with the
information you have posted here.
Please let me introduce you some info related to this post and I hope that it is useful for community.
There is a good C# resource site, Have alook
http://CSharpTalk.com
Thanks again
Rahul
By
rahul, at Wednesday, December 23, 2009 8:31:00 AM
Great solution, thank you. can you please clarify why the hosting assembly must be CLSCompliant? I get quite a few warnings with this enabled.
By
Georges POLITIS, at Tuesday, August 17, 2010 4:46:00 AM
@Georges POLITIS:
Hmm - I don't remember, but I suspect it had something to do with either an FXCop or Stylecop demand. I honestly don't know why it would be required.
By
Oskar Austegard, at Tuesday, August 17, 2010 8:35:00 AM
Oskar,
I know this is an old post and you probably don't think about this much anymore, but I recently posted something similar on my blog (.Net Object Formatter).
I posted a similar comment on Phil Haack's blog post.
I put the ObjectFormatter on github and was hoping you'd be interested in contributing.
I know it's a common problem and would love to see something robust come of a collaborative effort.
Thanks,
Patrick Caldwell
By
D. Patrick Caldwell, at Tuesday, August 23, 2011 11:42:00 AM
Post a Comment
<< Home