Wednesday, June 11, 2008

Opening web part’s editor zone in dialog window (improvements)

For first, this idea and main peice of code had been taken from Darren Neimke's blog post. But after some experiments I found that this method can be little improved:

In original post only one user in same time can edit web part's properties

To do this, it is logical to change PersonalizationScope from Shared to User in our DialogEditor.aspx.

<platform:CustomWebPartManager runat="server" id="WebPartManager1">
    <Personalization InitialScope="User" ProviderName="AspNetSqlPersonalizationProvider"></Personalization>
</platform:CustomWebPartManager>

BUT with current implementation we will find that in method ExportWebPart, when we are receiving exported XML from DialogEditor, in line after receiving WebPartManager is still ….. Shared….

public static string ExportWebPart(string wpid, string path, HttpContext context)
        {
            StringBuilder sb = new StringBuilder();

            string virtualPath = path;
            if (virtualPath.Contains("?"))
            {
                virtualPath = virtualPath.Substring(0, virtualPath.IndexOf("?"));
            }

            Page page = (Page)BuildManager.CreateInstanceFromVirtualPath(virtualPath, typeof(Page));

            page.Load += delegate
            {
                WebPartManager wm = WebPartManager.GetCurrentWebPartManager(page);

                if (wm.WebParts.Count > 0)
                {
                    WebPart part;
                    if (wpid != null)
                        part = wm.WebParts[wpid];
                    else
                        part = wm.WebParts[0];

                    if (part.ExportMode != WebPartExportMode.None)
                    {
                        using (StringWriter sw = new StringWriter(sb))
                        using (XmlTextWriter xw = new XmlTextWriter(sw))
                        {
                            wm.ExportWebPart(part, xw);
                        }
                    }
                }
            };

            ExecutePage(page, path, context);
            return sb.ToString();
        }

This problem occurs because we use Execute method that is store previous http context and previous page with WebPartManager placed on this page, and if you look with Reflector to PersonalizationProvider’s  method DetermineInitialScope you will find next code:

    else if ((page.PreviousPage != null) && !page.PreviousPage.IsCrossPagePostBack)
       {
           WebPartManager currentWebPartManager = WebPartManager.GetCurrentWebPartManager(page.PreviousPage);
           if (currentWebPartManager != null)
           {
               initialScope = currentWebPartManager.Personalization.Scope;
           }
       }

So – we use PersonalizationScope of WebPartManager from previous page.

To avoid this – we can use many way, write custom PersonalizationProvider for this DialogEditor.asxp page or write, for example, webservice that will do exactly same actions but of course does not contains any WebPartManagers. Last way is shown bottom.

[WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [ToolboxItem(false)]
    // To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line.
    // [System.Web.Script.Services.ScriptService]
    public class AdminWS : WebService
    {

        [WebMethod(true)]
        public string GetWPXml()
        {
            StringBuilder sb = new StringBuilder();
            Page page = (Page)BuildManager.CreateInstanceFromVirtualPath("~/DialogEditor.aspx", typeof(Page));

            page.Load += delegate
            {
                WebPartManager wm = WebPartManager.GetCurrentWebPartManager(page);

                if (wm.WebParts.Count > 0)
                {
                    WebPart part;
                    part = wm.WebParts[0];

                    if (part.ExportMode != WebPartExportMode.None)
                    {
                        using (StringWriter sw = new StringWriter(sb))
                        using (XmlTextWriter xw = new XmlTextWriter(sw))
                        {
                            wm.ExportWebPart(part, xw);
                        }
                    }
                }
            };

            ExecutePage(page, "~/DialogEditor.aspx", this.Context);
            return sb.ToString();
        }

        private static void ExecutePage(Page page, string path, HttpContext context)
        {
            string originalPath = context.Request.Path;
            context.RewritePath(path);
            context.Server.Execute(page, TextWriter.Null, false);
            context.RewritePath(originalPath);
        }

    }

And we should make some changes in RaisePostBackEvent method of ours webpart:

        public void RaisePostBackEvent(string eventArgument)
        {
            if (eventArgument == "whatever")
            {
                InternalAdminWS.AdminWS adminws = new InternalAdminWS.AdminWS();
                adminws.CookieContainer = new CookieContainer();
                adminws.CookieContainer.Add(new Cookie(FormsAuthentication.FormsCookieName, Request.Cookies[FormsAuthentication.FormsCookieName].Value, Request.Cookies[FormsAuthentication.FormsCookieName].Path, Request.Url.Host));
                adminws.Url = Request.Url.Scheme + "://" + Request.Url.Authority + "/AdminWS.asmx";
                adminws.Credentials = CredentialCache.DefaultNetworkCredentials;
                string xml = adminws.GetWPXml();
                //string xml = WebPartHelper.ExportWebPart(null, "~/DialogEditor.aspx", this.Context);
                if (!string.IsNullOrEmpty(xml))
                {
                    WebPartHelper.CopyWebPartValues(xml, this);
                }
            }
        }

Also I had make little changes to original CopyWebPartValues method – because when webpart properties are exported within ExportWebPart method – it use TypeConverter’s so to avoid custom conversion of every property type I had changed this method:

public static void CopyWebPartValues(string webPartXML, Control copyTo)
{

    XmlDocument doc = new XmlDocument();
    doc.LoadXml(webPartXML);
    XmlNamespaceManager mgr = new XmlNamespaceManager(doc.NameTable);
    mgr.AddNamespace("wp", "http://schemas.microsoft.com/WebPart/v3");

    foreach (XmlNode node in doc.SelectNodes(@"//wp:data/wp:properties/wp:property", mgr))
    {
        string propertyName = node.Attributes["name"].Value;
        string propertyValue = (node.FirstChild == null) ? "" : node.FirstChild.Value;
        PropertyInfo prop = copyTo.GetType().GetProperty(propertyName);

        Type propertyType = prop.PropertyType;

        // do we have a nullable type?
        if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
        {
            NullableConverter nc = new NullableConverter(propertyType);
            propertyType = nc.UnderlyingType;
        }

        TypeConverter converter = null;
        if (prop != null)
        {
            TypeConverterAttribute attribute = Attribute.GetCustomAttribute(prop, typeof(TypeConverterAttribute), true) as TypeConverterAttribute;
            if (attribute != null)
            {
                Type type = BuildManager.GetType(attribute.ConverterTypeName, false);

                if ((type != null) && type.IsSubclassOf(typeof(TypeConverter)))
                {
                    TypeConverter converter2 = (TypeConverter)Activator.CreateInstance(type);
                    if (converter2.CanConvertFrom(typeof(string)) && converter2.CanConvertTo(typeof(string)))
                    {
                        converter = converter2;
                    }
                }
            }
        }
        if (converter == null)
        {
            TypeConverter converter3 = TypeDescriptor.GetConverter(propertyType);
            if (converter3.CanConvertFrom(typeof(string)) && converter3.CanConvertTo(typeof(string)))
            {
                converter = converter3;
            }
        }
        try
        {
            object result = converter.ConvertFrom(propertyValue);
            prop.SetValue(copyTo, result, null);
        } catch (Exception ex)
        {
            log.Info(string.Format("Can't parse property type {0} with name {1}/nWith Exception : {2}", prop.PropertyType.Name, propertyName, ex));
        }
    }
}