/* * Document.cs - Represents the DesignerHost's document * * Authors: * Michael Hutchinson * * Copyright (C) 2005 Michael Hutchinson * * This sourcecode is licenced under The MIT License: * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to permit * persons to whom the Software is furnished to do so, subject to the * following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE * USE OR OTHER DEALINGS IN THE SOFTWARE. */ using System; using System.Text; using System.Web.UI; using System.Web.UI.WebControls; using System.IO; using System.ComponentModel.Design; using System.Collections; using AspNetEdit.Editor.Persistence; using System.ComponentModel; using AspNetEdit.Editor.ComponentModel; using System.Globalization; using AspNetEdit.Editor.UI; using System.Reflection; namespace AspNetEdit.Editor.ComponentModel { public class Document { public static readonly string newDocument = "\n\n\t{0}\n\n\n
\n\n
\n"; public static readonly string ControlSubstituteStructure = "{5}"; public static readonly string DirectivePlaceholderStructure = ""; string document; Hashtable directives; private int directivePlaceholderKey = 0; private Control parent; private DesignerHost host; private RootDesignerView view; private DesignTimeParser aspParser; ///Creates a new document public Document (Control parent, DesignerHost host, string documentName) { initDocument (parent, host); this.document = String.Format (newDocument, documentName); GetView (); } ///Creates a document from an existing file public Document (Control parent, DesignerHost host, string document, string fileName) { initDocument (parent, host); this.document = DeserializeAndAdd (document); GetView (); } private void initDocument (Control parent, DesignerHost host) { System.Diagnostics.Trace.WriteLine ("Creating document..."); if (!(parent is WebFormPage)) throw new NotImplementedException ("Only WebFormsPages can have a document for now"); this.parent = parent; this.host = host; if (!host.Loading) throw new InvalidOperationException ("The document cannot be initialised or loaded unless the host is loading"); CaseInsensitiveHashCodeProvider provider = new CaseInsensitiveHashCodeProvider(CultureInfo.InvariantCulture); CaseInsensitiveComparer comparer = new CaseInsensitiveComparer(CultureInfo.InvariantCulture); directives = new Hashtable (provider, comparer); this.aspParser = new DesignTimeParser (host, this); } private void GetView () { IRootDesigner rd = (IRootDesigner) host.GetDesigner (host.RootComponent); this.view = (RootDesignerView) rd.GetView (ViewTechnology.Passthrough); view.BeginLoad (); System.Diagnostics.Trace.WriteLine ("Document created."); } #region Some Gecko communication stuff //we don't want to have the document lying around forever, but we //want the RootDesignerview to be able to get it when Gecko XUL loads public string GetLoadedDocument () { if (document == null) throw new Exception ("The document has already been retrieved"); //TODO: substitute all components string doc = document; document = null; return doc; } ///Serialises the entire document to ASP.NET code public string PersistDocument () { StringBuilder builder = new StringBuilder(this.Serialize (view.GetDocument ())); //insert all remaining directives for (int i = 0; i <= directivePlaceholderKey; i++) { builder.Insert (0, RemoveDirective(i)); } return builder.ToString (); } public void DoCommand (string editorCommand) { view.DoCommand (editorCommand); } #endregion #region Serialisation stuff ///Converts a designer document fragment to ASP.NET code public string Serialize (string designerDocumentFragment) { if (host == null) throw new Exception("The document cannot be persisted without a host"); string serializedDoc = string.Empty; StringWriter writer = new StringWriter (); //keep method argument meaningfully named, but keep code readable! string frag = designerDocumentFragment; int length = frag.Length; int pos = 0; SMode mode = SMode.Free; while (pos < length) { char c = frag [pos]; switch (mode) { //it's freely copying to output, but watching for a directive or control placeholder case SMode.Free: if (c == '<') { if ((pos + 10 < length) && frag.Substring (pos + 1, 10) == "aspcontrol") { mode = SMode.ControlId; pos += 10; break; } else if ((pos + 20 < length) && frag.Substring (pos + 1, 20) == "directiveplaceholder") { mode = SMode.DirectiveId; pos += 20; break; } } writer.Write (c); break; //it's found a directive placeholder and is scanning for the ID case SMode.DirectiveId: if (c == 'i' && (pos + 4 < length) && frag.Substring (pos, 4) == "id=\"") { int idEnd = frag.IndexOf ('"', pos + 4 + 1); if (idEnd == -1) throw new Exception ("Identifier was unterminated"); int id = Int32.Parse (frag.Substring (pos + 4, (idEnd - pos - 4))); //TODO: more intelligent removal/copying of directives in case of fragments //works fine with whole document. string directive = RemoveDirective (id); writer.Write (directive); mode = SMode.DirectiveEnd; pos = idEnd; } break; //it's found a control placeholder and is scanning for the ID case SMode.ControlId: if (c == 'i' && (pos + 4 < length) && frag.Substring (pos, 4) == "id=\"") { int idEnd = frag.IndexOf("\"", pos + 4); if (idEnd == -1) throw new Exception ("Identifier was unterminated"); string id = frag.Substring (pos + 4, (idEnd - pos - 4)); DesignContainer dc = (DesignContainer) host.Container; Control control = dc.GetComponent (id) as Control; if (control == null) throw new Exception ("Could not retrieve control "+id); ControlPersister.PersistControl (writer, control); mode = SMode.ControlEnd; pos = idEnd; } break; //it's found the control's ID and is looking for the end case SMode.ControlEnd: if (c == '<' && (pos + 13 < length) && frag.Substring (pos, 13) == "") { pos += 12; mode = SMode.Free; } break; //it's found the placeholder's ID and is looking for the end case SMode.DirectiveEnd: if (c == '/' && (pos + 2 < length) && frag.Substring (pos, 2) == "/>") { pos += 1; mode = SMode.Free; } break; } pos++; } serializedDoc = writer.ToString (); writer.Close (); return serializedDoc; } ///Converts a ASP.NET fragment to a a designer document fragment, /// and adds the controls and directives etc to the host. public string DeserializeAndAdd (string aspFragment) { string document; Control[] controls; aspParser.ParseDocument (aspFragment, out controls, out document); foreach (Control c in controls) { OnInitMethodInfo.Invoke (c, new object[] {EventArgs.Empty}); } return document; } //modes for the Serializing parser private enum SMode { Free, ControlId, DirectiveId, ControlEnd, DirectiveEnd } //we need this to invoke protected member before rendering private static MethodInfo onPreRenderMethodInfo; private static MethodInfo OnPreRenderMethodInfo { get { if (onPreRenderMethodInfo == null) onPreRenderMethodInfo = typeof (Control).GetMethod ("OnPreRender", BindingFlags.NonPublic|BindingFlags.Instance); return onPreRenderMethodInfo; } } ///Renders the designer html for an ASP.NET Control public static string RenderDesignerControl (Control control) { string height = "auto"; string width = "auto"; string canResize = "true"; string canDrop = "false"; string id = control.UniqueID; WebControl wc = control as WebControl; if (wc != null) { height = wc.Height.ToString (); width = wc.Width.ToString (); } else { canResize = "false"; } //TODO: is there a better way to make tiny controls appear a decent size? if (height == "" || height == "auto") height = "20px"; if (width == "" || width == "auto") width = "20px"; //render the control //TODO: use designer, when they're written OnPreRenderMethodInfo.Invoke (control, new object[] {EventArgs.Empty}); System.IO.StringWriter strWriter = new System.IO.StringWriter (); System.Web.UI.HtmlTextWriter writer = new System.Web.UI.HtmlTextWriter (strWriter); control.RenderControl (writer); writer.Close (); strWriter.Flush (); string content = strWriter.ToString (); strWriter.Close (); return string.Format (ControlSubstituteStructure, id, width, height, canDrop, canResize, content); } #endregion //we need this to invoke protected member before rendering private static MethodInfo onInitMethodInfo; private static MethodInfo OnInitMethodInfo { get { if (onInitMethodInfo == null) onInitMethodInfo = typeof (Control).GetMethod ("OnInit", BindingFlags.NonPublic|BindingFlags.Instance); return onInitMethodInfo; } } #region add/remove/update controls public void AddControl (Control control) { OnInitMethodInfo.Invoke (control, new object[] {EventArgs.Empty}); view.AddControl (control); } public void RemoveControl (Control control) { view.RemoveControl (control); } public void RenameControl (string oldName, string newName) { view.RenameControl (oldName, newName); } public void InsertFragment (string fragment) { view.InsertFragment (fragment); } #endregion private string ConstructErrorDocument (string errorTitle, string errorDetails) { return "

" + errorTitle + "

" + errorDetails + "

"; } #region Add/fetch general directives /// /// Adds a directive port tracking. /// /// A placeholder identifier that can be used in the document public string AddDirective (string name, IDictionary values) { if ((0 == String.Compare (name, "Page", true, CultureInfo.InvariantCulture) && directives["Page"] != null) || (0 == String.Compare (name, "Control", true, CultureInfo.InvariantCulture) && directives["Control"] != null)) throw new Exception ("Only one Page or Control directive is allowed in a document"); DocumentDirective directive = new DocumentDirective (name, values, directivePlaceholderKey); directivePlaceholderKey++; if (directives[name] == null) directives[name] = new ArrayList (); ((ArrayList)directives[name]).Add(directive); return String.Format(DirectivePlaceholderStructure, directive.Key.ToString ()); } public string RemoveDirective (int placeholderId) { DocumentDirective directive = null; foreach (DictionaryEntry de in directives) { if (de.Value is DocumentDirective) { if (((DocumentDirective)de.Value).Key == placeholderId) { directive = (DocumentDirective)de.Value; directives.Remove(de.Key); } } else foreach (DocumentDirective d in (ArrayList)de.Value) if (d.Key == placeholderId) { directive = d; ((ArrayList)de.Value).Remove (d); break; } if (directive != null) break; } if (directive == null) return string.Empty; return directive.ToString(); } /// /// Gets all of the directives of a given type /// public DocumentDirective[] GetDirectives (string directiveType) { ArrayList localDirectiveList = new ArrayList (); foreach (DictionaryEntry de in directives) { if (de.Value is DocumentDirective) { if (0 == string.Compare (((DocumentDirective)de.Value).Name, directiveType, true, CultureInfo.InvariantCulture)) localDirectiveList.Add (de.Value); } else foreach (DocumentDirective d in (ArrayList)de.Value) if (0 == string.Compare (directiveType, d.Name, true, CultureInfo.InvariantCulture)) localDirectiveList.Add (d); } return (DocumentDirective[]) localDirectiveList.ToArray (typeof (DocumentDirective)); } /// /// Gets the first directive of a given type /// /// Whether the directive should be created if one does not already exist public DocumentDirective GetFirstDirective (string directiveType, bool create) { foreach (DictionaryEntry de in directives) { if (de.Value is DocumentDirective) { if (0 == string.Compare (((DocumentDirective)de.Value).Name, directiveType, true, CultureInfo.InvariantCulture)) return (DocumentDirective) de.Value ; } else foreach (DocumentDirective d in (ArrayList)de.Value) if (0 == string.Compare (d.Name, directiveType, true, CultureInfo.InvariantCulture)) return d; } //should directive be created if it can't be found? if (create) { AddDirective (directiveType, null); return GetFirstDirective (directiveType, false); } return null; } #endregion } }