/// <summary> /// Construct a new blob of metadata for a single control, performing any reflection needed to determine its /// structure and parsing behavior. /// </summary> /// <param name="compileContext">The context in which errors should be reported.</param> /// <param name="tag">The complete tag for this control, as found in the markup.</param> /// <param name="tagRegistrations">The known list of registrations, formed from the directives in the /// "web.config" and any <%@ Register %> directives in the markup.</param> /// <param name="assemblies">The complete list of known pre-loaded assemblies for reflection.</param> /// <param name="allowedTypes">The allowable types of control that may be returned. If the matching /// .NET class type for this control does not match one of these types (or is not derivable from one /// of these types), this constructor will throw an exception.</param> public ReflectedControl(ICompileContext compileContext, Tag tag, IEnumerable <TagRegistration> tagRegistrations, AssemblyLoader assemblies, IEnumerable <Type> allowedTypes) { // Decode the tag declaration. DecodeFullTagNameWithPrefix(tag.TagName, out TagPrefix, out TagName); // Find the matching C# type for that tag declaration. if (string.IsNullOrEmpty(TagPrefix)) { TagRegistration = _htmlTagRegistration; ControlType = FindMatchingHtmlControlType(tag); } else { ControlType = FindMatchingControlType(TagPrefix, TagName, tagRegistrations, assemblies, out TagRegistration); } if (ControlType == null) { throw new InvalidOperationException(string.Format("No matching type for <{0}> was found in any of the loaded assemblies.", tag.TagName)); } // If we are restricted to only load certain types (such as the nested not-a-control instances inside a DataPager control), // check that the control we have found matches one of those types. if (!allowedTypes.Any(t => t.IsAssignableFrom(ControlType))) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendFormat("Found matching type for <{0}>, but it is a {1}, not one of the {2} allowed types:\r\n", tag.TagName, ControlType.FullName, allowedTypes.Count()); foreach (Type allowedType in allowedTypes) { stringBuilder.AppendFormat("- {0}\r\n", allowedType.FullName); } throw new InvalidOperationException(stringBuilder.ToString()); } // Extract the [ParseChildren] attribute, if it has one. System.Web.UI.ParseChildrenAttribute[] parseChildrenAttributes = (System.Web.UI.ParseChildrenAttribute[])ControlType.GetCustomAttributes(typeof(System.Web.UI.ParseChildrenAttribute), true); ParseChildrenAttribute = parseChildrenAttributes.Length == 0 ? null : parseChildrenAttributes[0]; // Extract the [ControlBuilder] attribute, if it has one. System.Web.UI.ControlBuilderAttribute[] controlBuilderAttributes = (System.Web.UI.ControlBuilderAttribute[])ControlType.GetCustomAttributes(typeof(System.Web.UI.ControlBuilderAttribute), true); ControlBuilderAttribute = controlBuilderAttributes.Length == 0 ? null : controlBuilderAttributes[0]; // Extract the type's properties, since their declarations control what's legal in the markup. ControlProperties = CollectControlProperties(compileContext, ControlType, this); // HtmlControls have to be handled specially, since they have [ParseChildren(true)] in many cases but // aren't really using it for anything. if (ControlType.Namespace == _htmlTagRegistration.Namespace) { ParseChildrenAttribute = new ParseChildrenAttribute(false); } // Validate the ParseChildrenAttribute, which may be broken or weird. if (ParseChildrenAttribute != null) { if (!string.IsNullOrEmpty(ParseChildrenAttribute.DefaultProperty)) { string propertyName = ParseChildrenAttribute.DefaultProperty.ToLower(); // ASP.NET also ignores case on this; see internals of ControlBuilder.CreateChildBuilder() for details. if (!ControlProperties.ContainsKey(propertyName)) { throw new InvalidOperationException(string.Format("The [ParseChildren] attribute on class \"{0}\" names a default property \"{1}\" that does not exist in that class.", ControlType.FullName, ParseChildrenAttribute.DefaultProperty)); } DefaultCollectionProperty = ControlProperties[propertyName]; if (!DefaultCollectionProperty.IsCollectionProperty) { throw new InvalidOperationException(string.Format("The [ParseChildren] attribute on class \"{0}\" names a default property \"{1}\" that is not a collection property. The default property must always be a collection property.", ControlType.FullName, ParseChildrenAttribute.DefaultProperty)); } } } }
/// <summary> /// Search through the given set of tag registrations and assemblies to find a suitable .NET class type that /// matches the given tag prefix and tag name. (This method handles server controls and user controls, not HTML /// controls; the tag prefix must not be empty.) /// </summary> /// <param name="tagPrefix">The tag prefix of the tag we're searching for.</param> /// <param name="tagName">The tag name of the tag we're searching for.</param> /// <param name="tagRegistrations">The known list of registrations, formed from the directives in the /// "web.config" and any <%@ Register %> directives in the markup.</param> /// <param name="assemblies">The complete list of known pre-loaded assemblies for reflection.</param> /// <param name="matchingTagRegistration">The matching tag registration for this tag prefix/name, if there is one.</param> /// <returns>The matching class type, if one exists, or null if there is no matching class type.</returns> private static Type FindMatchingControlType(string tagPrefix, string tagName, IEnumerable <TagRegistration> tagRegistrations, AssemblyLoader assemblies, out TagRegistration matchingTagRegistration) { // Since multiple tag registrations could match this declaration, we have no choice but to walk through // all of the tag registrations and just try to find the first one that matches. foreach (TagRegistration tagRegistration in tagRegistrations) { // Try the prefix first. If that doesn't match, it's definitely not what we're looking for. if (string.Compare(tagRegistration.TagPrefix, tagPrefix, StringComparison.InvariantCultureIgnoreCase) != 0) { continue; } switch (tagRegistration.Kind) { case TagRegistrationKind.SingleUserControl: if (string.Compare(tagRegistration.TagName, tagName, StringComparison.InvariantCultureIgnoreCase) == 0) { // We matched a registered user control, so we have to partially parse it to find out // what namespace in the website assembly is associated with it. Assembly assembly = assemblies.PrimaryAssembly; Type type = assembly.GetType(tagRegistration.Typename); // Make sure we found it, and that it's actually a UserControl of some kind. if (type == null || !typeof(System.Web.UI.UserControl).IsAssignableFrom(type)) { break; } // We found it, so return it. matchingTagRegistration = tagRegistration; return(type); } break; case TagRegistrationKind.Namespace: { // This registration describes an entire namespace worth of controls in an assembly somewhere. // So check see if tagName matches a class inside the given namespace in that assembly. Assembly assembly = assemblies[tagRegistration.AssemblyFilename]; Type type = assembly.GetType(tagRegistration.Namespace + "." + tagName, false, true); // Make sure we found it. if (type == null) { break; } // We found it, so return it. matchingTagRegistration = tagRegistration; return(type); } case TagRegistrationKind.HtmlControl: // Shouldn't be able to get here. throw new InvalidOperationException("Internal error; got an HtmlControl when attempting to process a registered user control or namespace."); } } matchingTagRegistration = null; return(null); }
/// <summary> /// Construct a new control collection that will house all controls defined by the given set of /// registrations and assemblies. /// </summary> /// <param name="tagRegistrations">The tag registrations (from either the "web.config" file or /// from <%@ Register %%gt; directives in the markup).</param> /// <param name="assemblies">The set of known assemblies, preloaded.</param> public ReflectedControlCollection(IEnumerable <TagRegistration> tagRegistrations, AssemblyLoader assemblies) { _tagRegistrations = tagRegistrations; _assemblies = assemblies; }
/// <summary> /// For the given set of .aspx or .ascx files, generate all of their designer files. /// </summary> /// <param name="compileContext">The context in which errors are to be reported.</param> /// <param name="filenames">The filenames to generate.</param> /// <param name="rootPath">The root disk path of the website (usually the same as the path to "web.config").</param> /// <param name="websiteDllFileName">The disk path to the website's DLL.</param> public static bool GenerateDesignerFiles(ICompileContext compileContext, IEnumerable <string> filenames, string rootPath, string websiteDllFileName) { int filenameCount = filenames.Count(); compileContext.BeginTask(filenameCount); // Load and parse the "web.config". WebConfigReader webConfigReader = new WebConfigReader(); try { webConfigReader.LoadWebConfig(compileContext, Path.Combine(rootPath, WebConfigFilename), rootPath); } catch (Exception e) { compileContext.Error("Cannot load {0}:\r\n{1}", Path.Combine(rootPath, WebConfigFilename), e.Message); return(false); } // Load any assemblies we know we'll need. This includes the default assemblies, any declared // in the web.config, and, of course, the website's DLL itself. AssemblyLoader assemblyLoader = new AssemblyLoader(); List <string> assemblyNames = new List <string>(); assemblyNames.AddRange(_standardTagRegistrations.Where(r => !string.IsNullOrEmpty(r.AssemblyFilename)).Select(r => r.AssemblyFilename).Distinct()); assemblyNames.AddRange(webConfigReader.TagRegistrations.Where(r => !string.IsNullOrEmpty(r.AssemblyFilename)).Select(r => r.AssemblyFilename).Distinct()); string dllFullPath = Path.GetFullPath(websiteDllFileName); assemblyNames.Add(dllFullPath); string assemblyDirectory = Path.GetDirectoryName(dllFullPath); assemblyLoader.PreloadAssemblies(compileContext, assemblyNames, assemblyDirectory); assemblyLoader.PrimaryAssembly = assemblyLoader[dllFullPath]; // Add the default tag registrations, including those from System.Web and any declared in the "web.config". List <TagRegistration> tagRegistrations = new List <TagRegistration>(); tagRegistrations.AddRange(_standardTagRegistrations); tagRegistrations.AddRange(webConfigReader.TagRegistrations); // Spin through any user controls that were declared in the web.config and connect them to their actual // .NET class types via reflection. compileContext.Verbose("Resolving user controls declared in the web.config."); compileContext.VerboseNesting++; ResolveUserControls(compileContext, tagRegistrations, assemblyLoader, assemblyDirectory, rootPath, rootPath); compileContext.VerboseNesting--; compileContext.Verbose(string.Empty); // Now that all the setup is done, load and parse each individual markup file into its own .designer.cs output file. bool result = true; foreach (string filename in filenames) { compileContext.Verbose("Begin processing \"{0}\"...", filename); compileContext.Verbose(""); compileContext.VerboseNesting++; compileContext.BeginFile(filename); bool succeeded = GenerateDesignerForFilename(compileContext, filename, tagRegistrations, assemblyLoader, assemblyDirectory, rootPath); result &= succeeded; compileContext.EndFile(filename, succeeded); compileContext.VerboseNesting--; compileContext.Verbose(""); compileContext.Verbose("End processing \"{0}\".", filename); } return(result); }
/// <summary> /// Given a set of tag registrations for user controls, attempt to connect those tag registrations to actual /// .NET class types in the main website assembly. This will update the Typename field for each TagRegistration /// where a matching class type is found; or if no matching class type is found, this will throw an exception. /// </summary> /// <param name="compileContext">The context in which errors should be reported.</param> /// <param name="tagRegistrations">The set of user-control registrations to resolve to real class types.</param> /// <param name="assemblies">The full set of preloaded assemblies.</param> /// <param name="assemblyDirectory">The directory where the main website DLL can be found.</param> /// <param name="rootPath">The real disk path to the root of the website's virtual directory.</param> /// <param name="currentDirectory">The current directory (for resolving relative paths).</param> public static void ResolveUserControls(ICompileContext compileContext, IEnumerable <TagRegistration> tagRegistrations, AssemblyLoader assemblies, string assemblyDirectory, string rootPath, string currentDirectory) { foreach (TagRegistration tagRegistration in tagRegistrations.Where(t => t.Kind == TagRegistrationKind.SingleUserControl)) { compileContext.Verbose("Registering user control <{0}:{1}> as \"{2}\".", tagRegistration.TagPrefix, tagRegistration.TagName, tagRegistration.SourceFilename); compileContext.VerboseNesting++; string filename = ResolveWebsitePath(compileContext, tagRegistration.SourceFilename, rootPath, currentDirectory); MarkupReader userControlReader = new MarkupReader(); Tag userControlMainDirective = userControlReader.ReadMainDirective(compileContext, filename, assemblies, assemblyDirectory, rootPath); if (string.IsNullOrEmpty(userControlMainDirective.TagName) && string.Compare(userControlMainDirective.TagName, "control", StringComparison.InvariantCultureIgnoreCase) != 0) { throw new RedesignerException("Cannot register user control \"{0}\": Its main <% ... %> directive does not start with the \"Control\" keyword. Is this actually a user control?", tagRegistration.SourceFilename); } string inheritsAttribute = userControlMainDirective["inherits"]; if (string.IsNullOrEmpty(inheritsAttribute)) { throw new RedesignerException("Cannot register user control \"{0}\": Its main <% Control ... %> directive is missing the required Inherits=\"...\" attribute.", tagRegistration.SourceFilename); } tagRegistration.Typename = inheritsAttribute; compileContext.Verbose("User control registered as type \"{0}\".", inheritsAttribute); compileContext.VerboseNesting--; } }
/// <summary> /// Verify the current .designer.cs file for the given markup file. /// </summary> /// <returns>True if the file passes inspection, false if it fails.</returns> public static bool VerifyDesignerForFilename(ICompileContext compileContext, string filename, IEnumerable <TagRegistration> tagRegistrations, AssemblyLoader assemblies, string assemblyDirectory, string rootPath) { DesignerInfo designerInfo; string designerFilename = filename + ".designer.cs"; // Load the markup from the .aspx or .ascx file. MarkupReader markup = new MarkupReader(); MarkupInfo markupInfo; try { markupInfo = markup.LoadMarkup(compileContext, filename, tagRegistrations, assemblies, assemblyDirectory, rootPath); } catch (Exception e) { compileContext.Error("{0}: Failed to load markup file:\r\n{1}", filename, e.Message); compileContext.Verbose("Stopping file processing due to exception. Stack trace:\r\n{0}", e.StackTrace); return(false); } if (markupInfo.ClassType == null) { compileContext.Verbose("Skipping verification of .designer file, because markup has no Inherits=\"...\" attribute and therefore has no .designer file.", filename); return(true); } compileContext.Verbose(string.Empty); // Read and parse the current .designer.cs file. try { DesignerReader designerReader = new DesignerReader(); designerInfo = designerReader.LoadDesignerFile(compileContext, designerFilename); } catch (Exception e) { compileContext.Error("{0}: Cannot load designer file:\r\n{1}", filename, e.Message); compileContext.Verbose("Stopping file processing due to exception. Stack trace:\r\n{0}", e.StackTrace); return(false); } compileContext.Verbose(string.Empty); // And finally compare the expectations of the markup against the reality of the .designer.cs file. return(CompareMarkupInfoToDesignerInfo(compileContext, filename, markupInfo, designerInfo)); }
/// <summary> /// Generate a replacement .designer.cs file for the given markup file, overwriting the existing /// .designer.cs file if there is one. /// </summary> public static bool GenerateDesignerForFilename(ICompileContext compileContext, string filename, IEnumerable <TagRegistration> tagRegistrations, AssemblyLoader assemblies, string assemblyDirectory, string rootPath) { string designer; string designerFilename = filename + ".designer.cs"; // Load the markup from the .aspx or .ascx file. MarkupReader markup = new MarkupReader(); MarkupInfo markupInfo; try { markupInfo = markup.LoadMarkup(compileContext, filename, tagRegistrations, assemblies, assemblyDirectory, rootPath); } catch (Exception e) { compileContext.Error("{0}: Failed to load markup file:\r\n{1}", filename, e.Message); compileContext.Verbose("Stopping file processing due to exception. Stack trace:\r\n{0}", e.StackTrace); return(false); } // If we're not inheriting a real class, there's no reason for a designer file to exist. if (markupInfo.ClassType == null) { compileContext.Verbose("Skipping generating designer file because markup does not have an Inherits=\"...\" attribute.", filename); return(true); } // Generate the output text for the new .designer.cs file. try { DesignerWriter designerWriter = new DesignerWriter(); designer = designerWriter.CreateDesigner(compileContext, markupInfo); } catch (Exception e) { compileContext.Error("{0}: Cannot regenerate designer file:\r\n{1}", filename, e.Message); compileContext.Verbose("Stopping file processing due to exception. Stack trace:\r\n{0}", e.StackTrace); return(false); } // Save the output .designer.cs file to disk. try { File.WriteAllText(designerFilename, designer, Encoding.UTF8); } catch (Exception e) { compileContext.Error("{0}: Cannot open designer file for writing:\r\n{1}", designerFilename, e.Message); compileContext.Verbose("Stopping file processing due to exception. Stack trace:\r\n{0}", e.StackTrace); return(false); } return(true); }