Ah--an ongoing story. To see the code referenced on this page in action please goto https://ade.army.mil/tools/Pages/ServiceDiscovery.aspx.
First--a couple of disclaimers. All of my Webparts are hosted using a highly customized and extended form of Jan Tielens great "return of SmartPart". So all of my Webparts are actually ASCX user controls that get loaded dynamically. This means that I have a couple of different ways of getting into controls at init time, prerender time, etc. I have the customized SmartPart that I can leverage plus all of my controls derive from a well-defined hierarchy (which, of course, all of the rest of you are doing--right?). That is to say--only my base user control class actually derives from System.Web.UI.UserControl.
Now onto the problem. As no doubt you've read, the way to apply themes (read: "skins") in ASP.NET 2.0 is either with the Theme directive in the page source or by trapping PreInit and setting the theme there. Trouble is--that model just doesn't work with SharePoint Publishing sites. The reason is that the actual Page for Webpart pages is created dynamically at runtime. And solving the obvious way (via HttpModules, trapping BeginRequest, adding a new PreInit handler, and setting theme) doesn't work because under a Publishing site the "Page" available at BeginRequest time is *not* the Page actually generated. (Yes, this sounds screwy--internally SharePoint Publishing pages actually create an instance of the "real" page to process and call it without allowing the poor developer to insert code. Too bad for us!)
Well, it turns out there are two ways to skin this cat.
Way #1--Setting CssFilePath and CssPostfix at OnInit time (right after controls are created). What this means is that you first determine a mapping between SharePoint site styles and DevExpress themes (e.g. SharePoint site theme of Plastic kinda sorta maps to DevExpress theme of Soft Orange, SharePoint theme of Reflector is almost like DevExpress BlackGlass, and so on). So once you've done this then after creating your controls in OnInit, you can iterate over the control (iterate this.Controls recursively) looking for DevExpress.Web.ASPxClasses.ASPxWebControl-derived objects. Once you find one, you can apply some relatively simple logic (which I've included below) to set CssFilePath and CssPostfix appropriately. Tra-la--your DevExpress controls look Really Nice.
Except--for the dreaded ASPxRoundPanel (and maybe some others). The problem (I believe) is that DevExpress themes have explicit folders for the control type (e.g. Office2003 Blue/GridView, Office2003 Blue/HtmlEditor, etc.) but not a folder for RoundPanel. And the fallback stylings inside the Office2003 Blue/Web/styles.css are pretty minimal for ASPxRoundPanel:
/* -- ASPxRoundPanel -- */
.dxrpControl_Office2003_Blue td.dxrp
{
font: 9pt Tahoma, Verdana, Arial;
color: #000;
}
.dxrpControl_Office2003_Blue .dxrpHeader_Office2003_Blue td.dxrp
{
font-weight: bold;
}
This leaves the background color, borders, everything else as-is (the standard gray stuff unless you style it explicitly).
Which leads me to Way #2: Read, parse, and apply skin files programmatically. I'm kind of surprised I had to resort to this--my searches for "apply ASP.NET skin files programmatically" resulted in lots of bone-headed discussions on how great skins are and how to modify the Page directive to use a theme--which of course has nothing to do with applying skin files programmatically :).
So below is the code for reading a skin file at runtime, parsing it out, applying it to a DevExpress control programmatically using reflection. The code isn't really very optimized (I just wrote it this morning) but it works Pretty Good. The biggest problem I have appears to be an error in ASPxRoundPanel. In the Office 2003/ASPxRoundPanel.skin file when both width / height are set for any of TopLeftCorner, TopRightCorner, NoHeaderTopLeftCorner, and so on then the generated URL for the auto-generated IMG is wrong (it tries to reference an internal resource that doesn't exist). Removing either width or height (or both!) results in the correct URL being generated. The only problem then (sigh) is that the CSS filtering ( progid:DXImageTransform.Microsoft.AlphaImageLoader(src=[URL], sizingMethod=scale) isn't set as part of the auto-generated stylings so the PNG background doesn't default to the control background. And (of course) BackColor isn't a supported property for a PanelCornerPart so the result is a bit blocky.
You can take a look at what I've put together. Look carefully at the RoundPanel in the screen ("Authentication Information for the UDDI Server". You'll see the blocky corner edges (that's with width / height removed from .skin file) and also look at the bottom right corner of the RoundPanel. It's blank! That's because width / height are both specified in the .skin file and the auto-generated resource reference is not correct. (Use the IE Dev Toolbar to verify my statements).
Anyways, enuf with the yakking...here's some code! You'll have to modify it to remove the logging stuff (I doubt you want to use my whole class framework) but the logic is in place and actually working...
Once again, here's the link to see that the code works (feel free to try out the little UDDI tool as well...) https://ade.army.mil/tools/Pages/ServiceDiscovery.aspx
using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI;
using DevExpress.Web.ASPxCallback;
using DevExpress.Web.ASPxCallbackPanel;
using DevExpress.Web.ASPxClasses;
using DevExpress.Web.ASPxEditors;
using DevExpress.Web.ASPxGridView;
using DevExpress.Web.ASPxHtmlEditor;
using DevExpress.Web.ASPxPanel;
using DevExpress.Web.ASPxPopupControl;
using DevExpress.Web.ASPxRoundPanel;
using DevExpress.Web.ASPxTabControl;
namespace Escc.CodeLibrary.Utils
{
/// <summary>
/// Our wonderful third-party control toolkit suffers numerous design
/// flaws. Let's work around them here...
/// </summary>
public abstract class DevExpressHelpers
{
public static string DEVEXPRESS_THEME_USE_SHAREPOINT_SITE_MAPPING = "SharePointSiteMapping";
public static string DEVEXPRESS_THEME_USE_DEFAULT = "Default";
/// <summary>
/// Given source value string and a destination type, do coercion to get the
/// object to set.
/// </summary>
/// <param name="sourceValue">The source value to analyze</param>
/// <param name="destType">The destination type for coercion</param>
/// <returns>Either an object of the correct type or the provided source value</returns>
internal static object convertStringToType(string sourceValue, Type destType)
{
try
{
// explicit cases first (Color, Boolean, Unit)
if (destType == typeof(System.Drawing.Color))
{
return System.Drawing.ColorTranslator.FromHtml(sourceValue);
} //if
if (destType == typeof(System.Boolean))
{
return Convert.ToBoolean(sourceValue);
} //if
if (destType == typeof(System.Web.UI.WebControls.Unit))
{
System.Web.UI.WebControls.UnitConverter uc = new System.Web.UI.WebControls.UnitConverter();
return uc.ConvertFromString(sourceValue);
} //if
// general purpose enumeration conversion
if(destType.IsEnum) {
return Enum.Parse(destType, sourceValue);
} //if
// no conversion
return sourceValue;
}
catch
{
// can't convert...
return sourceValue;
} //try
} //convertStringToType
/// <summary>
/// Given a target object and an XML node, use simple reflection to apply property
/// values programmatically
/// </summary>
/// <param name="logger">Logging object</param>
/// <param name="oTarget">The destination object in which to set properties</param>
/// <param name="node">The XML node that defines the object properties to set</param>
internal static void applyXmlStylesToObject(
Escc.CodeLibrary.Utils.Logger.StandardLoggerBase logger,
object oTarget,
System.Xml.XmlNode node
)
{
// reflect on current attributes (assume each XML attribute corresponds one-to-one
// with a property on the target object)
System.Type type = oTarget.GetType();
foreach (System.Xml.XmlAttribute attr in node.Attributes)
{
// ignore items we don't want to set
if (attr.Name.ToLower().Equals("runat")) continue;
// set other items
System.Reflection.PropertyInfo propInfo = type.GetProperty(attr.Name);
if (propInfo != null)
{
try
{
// coerce as necessary
object oValue = convertStringToType(attr.Value, propInfo.PropertyType);
// set the type
propInfo.SetValue(oTarget, oValue, null);
}
catch (Exception ex)
{
logger.LogWarning_Low("Unable to set attribute ", attr.Name, " of type ", propInfo.PropertyType.Name, ": ", ex.ToString());
} //try
}
else
{
logger.LogWarning_Low("Unable to locate attribute ", attr.Name);
} //if
} //foreach
// iterate over child nodes (recursion)
if (node.HasChildNodes)
{
foreach (System.Xml.XmlNode nodeChild in node.ChildNodes)
{
// find the property for this entry
System.Reflection.PropertyInfo propInfo = type.GetProperty(nodeChild.Name);
if (propInfo != null)
{
object oChildTarget = null;
try
{
oChildTarget = propInfo.GetValue(oTarget, null);
}
catch (Exception ex)
{
logger.LogWarning_Low("Unable to find child node property ", nodeChild.Name, ": ", ex.ToString());
} //try
if (oChildTarget != null)
{
// we found a child node that corresponds to a property on the current
// target object. let's assign properties to that child target now.
applyXmlStylesToObject(logger, oChildTarget, nodeChild);
} //if
} //foreach
} //foreach
} //if
} //applyXmlStylesToObject
/// <summary>
/// Do programmatic ASP.NET skin file assignment to a given control. Useful when
/// an application framework (read: MOSS2007) goes way the heck out of its way
/// to prevent the poor application developer from trapping PreInit for the auto-generated
/// Page object. This occurs when you use a Publishing portal.
/// </summary>
/// <param name="logger">Logging object for output</param>
/// <param name="dxControl">The DevExpress control to use</param>
/// <param name="controlNameSansAspx">RoundPanel, Label, etc. (ASPx removed from front)</param>
/// <param name="cssFilePathName">The name of the DevExpress theme (Office2003 Blue, Plastic Blue, Red Wine, and so on)</param>
/// <returns>TRUE if the skin was found, parsed, and applied without errors to the control.</returns>
internal static bool handleSkin(
Escc.CodeLibrary.Utils.Logger.StandardLoggerBase logger,
DevExpress.Web.ASPxClasses.ASPxWebControl dxControl,
string controlNameSansAspx,
string cssFilePathName
)
{
string flag = "";
try
{
// build physical path to expected skin file
flag = "check_path";
string strPhysicalPath = System.Web.HttpContext.Current.Server.MapPath(
string.Format("/App_Themes/{0}/ASPx{1}.skin", cssFilePathName, controlNameSansAspx
));
if (!System.IO.File.Exists(strPhysicalPath))
{
// no skin--not a problem
logger.LogTrace_Trace("No skin available for '", cssFilePathName, ":", controlNameSansAspx, "'");
return false;
} //if
// read the skin file
flag = "read_skin";
string skinText = System.IO.File.ReadAllText(strPhysicalPath);
// look for @ Register
flag = "remove_register";
System.Text.RegularExpressions.Regex regexRegister = new System.Text.RegularExpressions.Regex(
"<%@\\s*Register[^%]+%>",
System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase
);
string skinTextNoRegister = regexRegister.Replace(skinText, "");
// remove any namespaces
flag = "remove_namespace";
System.Text.RegularExpressions.Regex regexNamespace = new System.Text.RegularExpressions.Regex(
"(</)?[A-Za-z_]+:",
System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase
);
string skinTextNoNamespace = regexNamespace.Replace(skinTextNoRegister, "$1");
// remove XML comments
flag = "remove_comments";
System.Text.RegularExpressions.Regex regexComment = new System.Text.RegularExpressions.Regex(
"<%--.*?--%?>",
System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase
);
string skinTextNoComments = regexComment.Replace(skinTextNoNamespace, "$1");
// read as XML
flag = "read_skin_as_xml";
string skinTextAsXml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" + System.Environment.NewLine + skinTextNoComments;
System.Xml.XmlDocument docSkin = new System.Xml.XmlDocument();
docSkin.LoadXml(skinTextAsXml);
// look for the control itself
flag = "select_control_from_xml";
System.Xml.XmlNode nodeControl = docSkin.SelectSingleNode(string.Format("/ASPx{0}", controlNameSansAspx));
if (nodeControl == null) throw new ApplicationException("Control is not found in skin file; ignoring");
// reflect on life a bit
flag = "performing_reflection";
applyXmlStylesToObject(logger, dxControl, nodeControl);
// the skinning worked
return true;
}
catch (Exception ex)
{
logger.LogError_High(
"Error at ", flag, ": control=", Escc.CodeLibrary.Utils.Global.SafeStr(dxControl.ID),
"; name=", controlNameSansAspx, "; filepath=", cssFilePathName, ": ", ex.ToString()
);
return false;
} //try
} //handleSkin
/// <summary>
/// Utility method to update a given control DevExpress elements with the specified
/// CSS file and stylings postfix. "Simple" (that's relative, of course) processing
/// is to assign CssFilePath and CssPostfix properties and call it done. "Complex"
/// processing is to search for, parse, and automatically use a .skin file if one
/// exists for the DevExpress control under our top-level App_Themes folder.
/// </summary>
/// <param name="owningSmartPart">The SmartPart-derived class that owns the control.</param>
/// <param name="control">The control in question</param>
/// <param name="cssFilePathName">The CSS file path to set for the control</param>
/// <param name="cssPostfix">The CSS postfix operator (e.g. "Soft_Orange") automatically used by DevExpress for stylings</param>
internal static void HandleDevExpressStyles(
Escc.CodeLibrary.Utils.Logger.StandardLoggerBase logger,
System.Web.UI.Control control,
string cssFilePathName,
string cssPostfix
)
{
// sanity
if (control == null) return;
DevExpress.Web.ASPxClasses.ASPxWebControl dxControl = control as DevExpress.Web.ASPxClasses.ASPxWebControl;
if (dxControl != null)
{
logger.LogTrace_Low(
"Applying style ", cssFilePathName, "; ", cssPostfix, " to DevExpress control ",
Escc.CodeLibrary.Utils.Global.SafeStr(dxControl.ID)
);
if (cssFilePathName.ToLower().Equals("default"))
{
logger.LogTrace_Low(
" Retaining default CssFilePath setting for ",
Escc.CodeLibrary.Utils.Global.SafeStr(dxControl.ID)
);
}
else
{
if (string.IsNullOrEmpty(dxControl.CssFilePath))
{
// do a bit of extra checking
string aspxName = dxControl.GetType().Name;
if (aspxName.ToLower().StartsWith("aspx")) aspxName = aspxName.Substring(4);
// if the type name doesn't exist on disk, then use the Web styles
string devExpressFilePathFmtPrefix = "/App_Themes/" + cssFilePathName + "/{0}/";
string devExpressFilePathFmt = devExpressFilePathFmtPrefix + "styles.css";
string devExpressImagesFmt = devExpressFilePathFmtPrefix;
string urlPathExplicit = string.Format(devExpressFilePathFmt, cssFilePathName);
string physicalPathExplicit = System.Web.HttpContext.Current.Server.MapPath(urlPathExplicit);
if (!System.IO.File.Exists(physicalPathExplicit))
{
logger.LogTrace_Medium(
"Explicit URL '", urlPathExplicit, "' (", physicalPathExplicit, ") is not available for , using Web"
);
//devExpressFilePathFmt = string.Format(devExpressFilePathFmt, "Web");
} //if
dxControl.CssFilePath = devExpressFilePathFmt;
// handle the skin if one exists
handleSkin(logger, dxControl, aspxName, cssFilePathName);
}
else
{
logger.LogTrace_Low(
" Retaining user-defined CssFilePath setting of '", dxControl.CssFilePath, "' for ",
Escc.CodeLibrary.Utils.Global.SafeStr(dxControl.ID)
);
} //if
} //if
if (cssPostfix.ToLower().Equals("default"))
{
logger.LogTrace_Low(
" Retaining default CssPostfix setting for ",
Escc.CodeLibrary.Utils.Global.SafeStr(dxControl.ID)
);
}
else if (string.IsNullOrEmpty(dxControl.CssPostfix))
{
dxControl.CssPostfix = cssPostfix;
}
else
{
logger.LogTrace_Low(
" Retaining user-defined CssPostfix setting of '", dxControl.CssPostfix, "' for ",
Escc.CodeLibrary.Utils.Global.SafeStr(dxControl.ID)
);
} //if
} //if
if (control.HasControls())
{
// recursive calls
foreach (System.Web.UI.Control child in control.Controls)
{
HandleDevExpressStyles(logger, child, cssFilePathName, cssPostfix);
} //foreach
} //if
} //HandleDevExpressStyles
/// <summary>
/// Utility method to apply stylings automagically to DevExpress controls
/// </summary>
/// <param name="owningSmartPart"></param>
public static void HandleDevExpressStyles(
Escc.CodeLibrary.Utils.ConfigReaders.BaseReader configReader,
Escc.CodeLibrary.Utils.Logger.StandardLoggerBase logger,
System.Web.UI.Control control
)
{
// get the configured mapping values from the SmartPart
IDevExpressThemeControl devExpressThemeControl = control as IDevExpressThemeControl;
string specificCssFilePathName = "default";
string specificCssPostfix = "default";
if (devExpressThemeControl != null)
{
if (devExpressThemeControl.HasSpecificCss(devExpressThemeControl))
{
// retrieve values
specificCssFilePathName = devExpressThemeControl.CssFilePathName(devExpressThemeControl);
specificCssPostfix = devExpressThemeControl.CssPostfix(devExpressThemeControl);
// if specified--do the mapping now
if (specificCssFilePathName.ToLower().Equals("default"))
{
// nothing else to to do--control wants to use default stylings
return;
} //if
if (!specificCssFilePathName.ToLower().Equals("SharePointSiteMapping".ToLower()))
{
// do specific mapping
HandleDevExpressStyles(logger, control, specificCssFilePathName, specificCssPostfix);
return;
} //if
} //if
} //if
// are we enabled?
if (!configReader.AppSettingBool("Escc.CodeLibrary.Utils.DevExpressHelpers.AutoFollowSharePointTheme.Enabled"))
{
logger.LogTrace_Low("Escc.CodeLibrary.Utils.DevExpressHelpers.AutoFollowSharePointTheme.Enabled is False; ignoring");
return;
} //if
// anything on SharePoint?
string spTheme = Microsoft.SharePoint.SPContext.Current.Web.Theme.ToLower();
if (string.IsNullOrEmpty(spTheme))
{
logger.LogTrace_Low("No SharePoint theme; using Default mapping");
spTheme = "Default".ToLower();
} //if
// get the mapping
string[ mapping_key_values = configReader.AppSetting("Escc.CodeLibrary.Utils.DevExpressHelpers.AutoFollowSharePointTheme.Mapping").Split(';');
if (mapping_key_values == null || mapping_key_values.Length == 0)
{
logger.LogTrace_Low("Escc.CodeLibrary.Utils.DevExpressHelpers.AutoFollowSharePointTheme.Mapping is empty; ignoring");
return;
} //if
// do the mapping
foreach (string mapping_key_value in mapping_key_values)
{
// must consist of key=value
string[ key_value = mapping_key_value.Split('=');
if (key_value == null || key_value.Length != 2) continue;
// the value must be the external theme name followed by internal theme name
string[ values = key_value[1].Split(',');
if (values == null || values.Length != 2)
{
logger.LogWarning_High("Escc.CodeLibrary.Utils.DevExpressHelpers.AutoFollowSharePointTheme.Mapping is invalid at key: ", key_value[0], ": '", key_value[1], "'");
continue;
} //if
// match the current theme?
if (key_value[0].ToLower().Equals(spTheme))
{
logger.LogInfo_Trace("Mapping SP theme of '", spTheme, "' to DevExpress App_Theme of '", key_value[1], "'");
HandleDevExpressStyles(logger, control, values[0], values[1]);
break;
} //if
} //foreach
} //HandleDevExpressStyles
/// <summary>
/// For auto-magic DevExpress control theme support based on current SharePoint site
/// theme--invoke this method for controls that implement the IDevExpressThemeControl
/// interface. The way it works is this: For any ESCC SmartPart-hosted ASCX control,
/// you implement the IDevExpressThemeControl and IUserDefinedEnumeration interfaces
/// and provide a SharePoint-visible control property with an
/// Escc.CodeLibrary.SmartPart.LinkLookupFieldAttribute setting of
/// LINK_LOOKUP_TYPES.USER_DEFINED_ENUMERATION. The ESCC SmartPart objects automatically
/// invoke the HasSpecificCss, the CssFilePathName, and the CssPostfix methods of the
/// IDevExpressThemeControl control to determine if a specific DevExpress theme has
/// been specified for the control. If so, it's auto-selected when the user modifies
/// the SmartPart-hosted control properties (using SharePoint's Edit Page / Modify
/// Settings functionality). The internal property stored by your control is actually
/// encoded to contain both the CssFilePath and the CssPostfix properties, so to keep
/// you from having to parse the value out yourself you should use this method.
/// </summary>
/// <param name="devExpressSpecificTheme">The internal coding of the DevExpress theme stored with this object</param>
/// <returns>The CssFilePath portion of the internal code</returns>
public static string CssFilePathNameFromStoredDevExpressTheme(string devExpressSpecificTheme)
{
if (string.IsNullOrEmpty(devExpressSpecificTheme)) return Escc.CodeLibrary.Utils.DevExpressHelpers.DEVEXPRESS_THEME_USE_SHAREPOINT_SITE_MAPPING;
string[ parts = devExpressSpecificTheme.Split(',');
if (parts == null || parts.Length != 2) return Escc.CodeLibrary.Utils.DevExpressHelpers.DEVEXPRESS_THEME_USE_SHAREPOINT_SITE_MAPPING;
return parts[0];
} //CssFilePathNameFromStoredDevExpressTheme
/// <summary>
/// For auto-magic DevExpress control theme support based on current SharePoint site
/// theme--invoke this method for controls that implement the IDevExpressThemeControl
/// interface. The way it works is this: For any ESCC SmartPart-hosted ASCX control,
/// you implement the IDevExpressThemeControl and IUserDefinedEnumeration interfaces
/// and provide a SharePoint-visible control property with an
/// Escc.CodeLibrary.SmartPart.LinkLookupFieldAttribute setting of
/// LINK_LOOKUP_TYPES.USER_DEFINED_ENUMERATION. The ESCC SmartPart objects automatically
/// invoke the HasSpecificCss, the CssFilePathName, and the CssPostfix methods of the
/// IDevExpressThemeControl control to determine if a specific DevExpress theme has
/// been specified for the control. If so, it's auto-selected when the user modifies
/// the SmartPart-hosted control properties (using SharePoint's Edit Page / Modify
/// Settings functionality). The internal property stored by your control is actually
/// encoded to contain both the CssFilePath and the CssPostfix properties, so to keep
/// you from having to parse the value out yourself you should use this method.
/// </summary>
/// <param name="devExpressSpecificTheme">The internal coding of the DevExpress theme stored with this object</param>
/// <returns>The CssPostfix portion of the internal code</returns>
public static string CssPostfixFromStoredDevExpressTheme(string devExpressSpecificTheme)
{
if (string.IsNullOrEmpty(devExpressSpecificTheme)) return Escc.CodeLibrary.Utils.DevExpressHelpers.DEVEXPRESS_THEME_USE_SHAREPOINT_SITE_MAPPING;
string[ parts = devExpressSpecificTheme.Split(',');
if (parts == null || parts.Length != 2) return Escc.CodeLibrary.Utils.DevExpressHelpers.DEVEXPRESS_THEME_USE_SHAREPOINT_SITE_MAPPING;
return parts[1];
} //CssPostfixFromStoredDevExpressTheme
} //DevExpressHelpers
} //namespace