Sharing the experience search

Search sharing-the-experience.blogspot.com

Thursday, January 27, 2011

Module < AllUsersWebPart : how to add the xslt webpart

Can you add a dataview webpart (a  XSLT webpart)  into the solution  and make it deployable?
Yes, you can! But first you need to be aware of the following:

1. SharePoint Designer will let you convert a simple list view webpart into XSLT webpart  really easy, but it will hardcode the id of the list. It makes you xslt webpart not deployable  to any other site.

2. In case  you know how (if you don't know - I will explain further)  to resolve the first issue, you need to implement module features with file element which has a  AllUsersWebPart element. In that element you need to insert you xslt web part. !You need to insert your imported xslt webpart.

3.Sometimes it is really hard to guess what went wrong during module activation and why web part won't appear. 

Once you are aware of it, we can run straight ahead to the implementation:

1. Open your SPD, the page where you want to add your xstl web part. And start crafting it:
option "create a dataview webpart":  place your cursor inside a webpartzone and select from the main menu : Insert->SharePoint Controls ->Data View
option "convert to a dataview" : select the listview webpart which is already  - right click - select in the context menu "Convert to a XSLT Data View". Make changes that you want to make it. Save it.
Here is you go  - your xslt webpart on the page, but it's not deployable.

2. To make this webpart deployable we have to add it to the module feature. Prior to adding -we need at first import the web part. Go to the page with your webpart in the browser. Click on a  right corner arrow and select "Import".  I would prefer on this phase  to test the webpart and export it back to the page. 

Recently I have killed almost half of the day trying to figure out why the web part is broken after the module feature activation. If I had tested before,  I would have known  that's because imported webpart has a wrong value for a  property (<property name="ViewFlag" type="string"></property>. I have deleted the value.)

Ok, as soon as the webpart can easily export back, you can proceed with the module feature.
 Refer to detail explanation how to provision a file to msdn.

  Let's assume that you have managed you way out and got you a module feature with the reference to a page you want to provision and element  <AllUsersWebPart>. Let me just point it out that you have to put under this element <AllUsersWebpart> -   <![CDATA[
{exported web part content starting with  <webParts>
  <webPart xmlns="http://schemas.microsoft.com/WebPart/v3">}]]>


Good deal! But it's still undeployable because the exported web part has a hard-coded value of the list Id.
Refer to out custom approach of an id replacement

Once you have handled  the id replacement, you are fearless and with deployable xslt web part on your site.

File provisioning: a hard-coded list id replacement

Howdy!
Today's topic might really help you in your SharePoint life. I will present to you a  technique to work with SPD workflow and page provisioning to make them portable from one environment to another. Not delaying any further - I will show you  real world examples with it's issue:
 Example #1. I have created SPD (SharePoint Designer ) workflow and want to export it and import to the another site created from the same site template (different enviroment: Dev, Qc, Staging).
 Issue: The exported wf holds hard-coded list id value
 Example #2. I have created a XSLT webpart and put it in a module <file> element. This feature should work on every site created from the site template to which this feature is belong to.
Issue: The xslt webpart has a hard-coded ListId.

Ok, I think it's convincing enough to read on to find a solution for described situations.
Here is our custom approach:
   We created a feature which we call SiteProvisioning. It consists of 2 elements:
1. A custom class: SiteProvisioning.cs which set as ReceiverClass for the feature:
<Feature Id="{4FA2AC90-321A-4d4c-B587-CE6D4A5B90FA}"
         Title="SiteProvisioning"
         Description=""
         Version="1.0.0.0"
         Scope="Web"
         Hidden="FALSE"
         DefaultResourceFile="core"
         ReceiverAssembly="Custom.SharePoint.Case, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a4607a0133bbaeb2"
         ReceiverClass="Custom.SharePoint.Case.SiteDefinition.SiteProvisioning"

         xmlns="http://schemas.microsoft.com/sharepoint/">
  <ActivationDependencies>
    <ActivationDependency FeatureId="43DA1A6A-0759-4093-B84D-9593555BC037"/>
  </ActivationDependencies>
( I will explain this element later)
  <ElementManifests>
    <ElementFile Location="SiteProvisioning.xml" />
  </ElementManifests>
</Feature>

2. And a file:
 <ElementManifests>
    <ElementFile Location="SiteProvisioning.xml" />
  </ElementManifests>
SiteProvisioning file is a list of elements which are originally have a hard-coded value and related list title;
<SiteSettings>
  <FixupFiles>
    <FixupFile DataViewInZone="true" DeleteWebPartsOnDeactivation="true" RelativePath="Lists/Discussion/DiscussionView.aspx" />
    <FixupFile DataViewInZone="true" DeleteOnDeactivation="true" RelativePath="default.aspx" />
    <FixupFile DataViewOutZone="true" DeleteOnDeactivation="true" RelativePath="default.aspx" />
  </FixupFiles>
  <ListInstances>
    <ListInstance Id="fd482aa7-9511-4b0f-abeb-79030bb681ca" Title="Case" />
    <ListInstance Id="85ac067e-e536-46f3-9a7b-032016cd032d" Title="Case Review Meeting" />
    <ListInstance Id="cde441a0-b34f-4f19-ba47-52b0b5c8d49e" Title="Action Items" />
    <ListInstance Id="dd764ce9-aa7a-41aa-aa2f-28841aee3356" Title="Claims" />
    <ListInstance Id="9b811048-cbd5-4e41-b526-703f1ddc3fae" Title="Disciplinary Actions" />
    <ListInstance Id="A8949622-30DB-4CE6-A6A6-B674DED51355" Title="Involved People" Web="/apps/systems/cases/" />
    <ListInstance Id="cbe513fb-ef61-45aa-9162-23398b8a4011" Title="Reports" />
    <ListInstance Id="4DF356F2-90CF-4D76-9B38-1B17F3B3D6E9" Title="Tasks" />
  </ListInstances>
</SiteSettings>

Every time we put a xslt web part in the module file, we need to be sure that we found a hard-coded value in it and added it to the SiteProvisioning also we need to put a file where we should replace this id during siteprovisioning.

The guy who will be in charge of the replacement is a  SiteProvisioning class
Here is an essence of it:

 public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            if (properties == null)
            {
                return;
            }

            SPWeb web = properties.Feature.Parent as SPWeb;         
            string filePath = this.GetProvisioinerFilePath(properties.Definition);

            var sz = new XmlSerializer(typeof(SiteSettings));
            SiteSettings settings = null;
            if (File.Exists(filePath)) {
                using (var sr = File.OpenRead(filePath)) {
                    try {
                        settings = sz.Deserialize(sr) as SiteSettings;
                    } catch { }
                }
            }

            if (settings != null) {
                this.RestoreDataViewInZone(web, settings);
                this.RestoreDataViewOutZone(web, settings);           
            }

            this.OnActivated(properties);
        }
  private void RestoreDataViewInZone(SPWeb web, SiteSettings settings)
        {
            if (settings.FixupFiles == null)
                return;

            foreach (var xFixupFile in settings.FixupFiles.Where(i => i.DataViewInZone))
            {
                var relativePath = xFixupFile.RelativePath;
                if (string.IsNullOrEmpty(relativePath))
                {
                    continue;
                }

                SPFile file = web.GetFile(relativePath);
                if (file == null)
                {
                    continue;
                }

                SPLimitedWebPartManager manager = file.GetLimitedWebPartManager(System.Web.UI.WebControls.WebParts.PersonalizationScope.Shared);
                SPLimitedWebPartCollection pageWebParts = manager.WebParts;
                if (pageWebParts == null)
                {
                    continue;
                }

                foreach (System.Web.UI.WebControls.WebParts.WebPart webPart in pageWebParts)
                {
                    if (((webPart is DataFormWebPart) && (((DataFormWebPart)webPart).ParameterBindings != null)))
                    {
                        this.SubstituteGuidInZone(web, manager, webPart as DataFormWebPart, settings);
                        this.SubstituteIDInZone(web, manager, webPart, settings);
                    }
                    else
                    {
                        this.SubstituteIDInZone(web, manager, webPart, settings);
                    }
                }
                // http://msdn.microsoft.com/en-us/library/aa973248.aspx
                //
                //  Microsoft.SharePoint.WebPartPages.SPLimitedWebPartManager
                //  The SPLimitedWebPartManager class contains a reference to an internal SPWeb object that must be disposed.
              
                manager.Web.Dispose();
            }
        }

 private void SubstituteGuidInZone(SPWeb web, SPLimitedWebPartManager manager, DataFormWebPart dataForm, SiteSettings settings)
        {
            if (settings.ListInstances == null)
            {
                return;
            }

            foreach (var xListInstance in settings.ListInstances)
            {
                if (xListInstance.Id == null || xListInstance.Title == null) {
                    return;
                }

                SPList list = null;
                try
                {
                    if (string.IsNullOrEmpty(xListInstance.Web))
                    {
                        list = web.Lists[xListInstance.Title];
                    }
                    else
                    {
                        using (SPWeb listWeb = web.Site.OpenWeb(xListInstance.Web))
                        {
                            list = listWeb.Lists[xListInstance.Title];
                        }
                    }
                }
                catch (ArgumentException)
                {
                    continue;
                }

                if (list == null)
                {
                    continue;
                }
                string newId = list.ID.ToString();


                dataForm.ListName = newId;
                dataForm.ParameterBindings = Regex.Replace(dataForm.ParameterBindings, xListInstance.Id, newId, RegexOptions.IgnoreCase);
                dataForm.DataSourcesString = Regex.Replace(dataForm.DataSourcesString, xListInstance.Id, newId, RegexOptions.IgnoreCase);
                dataForm.Xsl = Regex.Replace(dataForm.Xsl, xListInstance.Id, newId, RegexOptions.IgnoreCase);

                manager.SaveChanges(dataForm);
            }
        }

     private void SubstituteGuidInZone(SPWeb web, SPLimitedWebPartManager manager, DataFormWebPart dataForm, SiteSettings settings)
        {
            if (settings.ListInstances == null)
            {
                return;
            }

            foreach (var xListInstance in settings.ListInstances)
            {
                if (xListInstance.Id == null || xListInstance.Title == null) {
                    return;
                }

                SPList list = null;
                try
                {
                    if (string.IsNullOrEmpty(xListInstance.Web))
                    {
                        list = web.Lists[xListInstance.Title];
                    }
                    else
                    {
                        using (SPWeb listWeb = web.Site.OpenWeb(xListInstance.Web))
                        {
                            list = listWeb.Lists[xListInstance.Title];
                        }
                    }
                }
                catch (ArgumentException)
                {
                    continue;
                }

                if (list == null)
                {
                    continue;
                }
                string newId = list.ID.ToString();


                dataForm.ListName = newId;
                dataForm.ParameterBindings = Regex.Replace(dataForm.ParameterBindings, xListInstance.Id, newId, RegexOptions.IgnoreCase);
                dataForm.DataSourcesString = Regex.Replace(dataForm.DataSourcesString, xListInstance.Id, newId, RegexOptions.IgnoreCase);
                dataForm.Xsl = Regex.Replace(dataForm.Xsl, xListInstance.Id, newId, RegexOptions.IgnoreCase);

                manager.SaveChanges(dataForm);
            }
        }

Tuesday, January 25, 2011

BusinessDataField API: How to add a secondary field to an existing BDC field

   I wondering if people usually change the field structure on the existing list through the schema change. If you are the one who does it - welcome to the club! (But actually it's not supported by Microsoft. To read further about the  upgradability the existing schema refer to Sharepoint : how to survive after the first deployment)
   But as you know the "Here is ideal and here is real" rule shows up in the every change in the sharepoint site.
 Recently I had a task to add one more Secondary Field to BDC field (BusinessDataField ) to the existing list instance list. One may say - it' easy - just do it through UI. But in my case I have a lots of business logic incorporated code behind, and I  need to have control over the related WSS field to use in my code.
   To have control over the field, I prefer not to rely on the display name of the field, but on a static name which is unchangeable.
   My task is:
     1.add the new Secondary Field to existing BDC field on the list;
     2. make possible to manipulate with new secondary field through the code.
  Solutions:
     1. First approach  -  to change <Field Type="BusinessData" > the schema failed. The existing list didn't see the changes in the attributes like SecondaryFieldBdcNames,SecondaryFieldWssNames, SecondaryFieldsWssStaticNames.
         The second approach  did the job and created the secondary field, but here is a tricky part - the only method I can use doesn't give me power to control the static name of the field that it creates. But I will discuss in the part 2 of the solution how I can make do with an unpredictable static name of the secondary field of the business data field.
         Let me first  show  how you can set an additional secondary field to the existing business field on the list:

      SqlSessionProvider.Instance().SetSharedResourceProviderToUse(
                                                   CommonConstants.SharedServices(oWebsite.Site));
                                             
                                               string[] flds = field.GetSecondaryFieldsNames();

                                               List<string> lstFlds= new List<string>(flds);
                                               lstFlds.Add("New_Secondary_Field_Name");

                                               string[] newFlds = lstFlds.ToArray();
//I have noticed that people tend to reapply the existing properties of BussinesDataField to make sure there will not be lost after BussinesField.Update
                                               field.SystemInstanceName = "InstanceName";

                                               field.SetSecondaryFieldsNames(newFlds);

                                               field.EntityName = field.EntityName;
                                               field.BdcFieldName = field.BdcFieldName;
                                               field.HasActions = field.HasActions;
                                               field.Update();
 I want to emphasize that method BusinessDataField.SetSecondaryFieldsNames is not intended to be used directly from your code as Microsoft says.

2. Now I have a new secondary field  in the list.  I need to use it in my code. My last task - is to identify this added secondary field without knowing its static name. I wrote the method which searches through schema fields with an attribute "BdcField" and than trying to find the field with the passed SecondaryBDCValue (BDCFieldValue).

   public static string getWssFieldInternalNameByBDCFied (SPList list,BusinessDataField BDCField,  string BDCFieldValue )
        {
           
            string[] secFldNames;
            XmlDocument schemaBdc = new XmlDocument();
            schemaBdc.LoadXml(BDCField. SchemaXml);
            XmlNode bdc = schemaBdc.DocumentElement;

            if(bdc==null)
            {
                return null;
            }
            secFldNames = bdc.Attributes["SecondaryFieldsWssStaticNames"].Value.Split(':');
            SPFieldCollection flds = list.Fields;

            List<SPField> fldsWithBDCFieldValue = new List<SPField>();
            foreach (SPField f in flds)
            {  
                 string BdcProperty ="";
                try
                {
                   XmlDocument schema =new XmlDocument();
                   schema.LoadXml(f.SchemaXml);
                   XmlNode fld = schema.DocumentElement;

                    if(fld!=null && fld.Attributes["BdcField"]!=null)
                    {
                        BdcProperty = fld.Attributes["BdcField"].Value;
                    }
                }
                catch(Exception ex)
                {
                }
               
                if(BdcProperty.Equals(BDCFieldValue))
                {
                    fldsWithBDCFieldValue.Add(f);
                }

               
            }

            var wssFldInternalNames= from schemaFld in fldsWithBDCFieldValue
                    join secFld in secFldNames on schemaFld.StaticName equals secFld
                    select schemaFld.InternalName;

            string wssFldInternalName = wssFldInternalNames.FirstOrDefault();
          
               
            return wssFldInternalName;
}
 

SPD: Site cache : how to clear?

Sometimes the site opened in the SharePoint Designer doesn't reflect up to date changes.
I have discovered it  a  long time ago when were dabbling with SharePoint Designer Workflow- the new actions didn't want to appear in the list of available.

Recently I have noticed that SharePoint Designer didn't show recently customized pages in the report of customized pages.

The reason was the  SPD caches the opened site.

The cure is easy to apply but not so easy to find!

To clear the cache - you have to go to:

        C:\Users\<username>\AppData\Local\Microsoft\WebsiteCache
        C:\Users\<username>\AppData\Roaming\Microsoft\SharePoint Designer\ProxyAssemblyCache
        C:\Users\<username>\AppData\Roaming\Microsoft\Web Server Extensions\Cache


 - and delete all folders and files there;
 - re-start SPD.


Breathe with relief)

javascript: how to make Input non editable

Do you want dynamically set the input non-editable?
Here we go:

var setNonEditable=true;
var input =document.getElementById("InputId");
//#1 In this case - the value won't be save in the input field event it presents there
if(setNonEditable)
 {
    input .style.visibility = 'hidden';
 }
else
 {
   input .style.visibility = 'visible';
  }
//#2 The value will be saved even the input is not editable but value is in there
if(setNonEditable)
 {
   input .disabled = true;
 }
else
 {
     input .disabled = false;
  }

Happy easy coding!

Friday, January 7, 2011

SPItemEventReceiver < Data >: how to use it and where

I m sure you aware of this piece of xml code if you ever use the custom SPItemEventReceiver :
<Elements><Receivers> <Receiver>
Every time you want to associate your custom EventReceiver with a specific list you have to create a feature with element which will describe the association :
<Receivers ListTemplateId="{your listtemplate id}">
    <Receiver>
      <Name>ItemAddedAutoTitleEventHandler</Name>
      <Type>ItemAdded</Type>
      <SequenceNumber>10000</SequenceNumber>
      <Assembly>SE.SharePoint.blog, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a4607a0133bbaeb2</Assembly>
      <Class>SE.SharePoint.blog.EventHandlers.AutoPropagateTitleEventReceiver</Class>
      <Data></Data>
      <Filter></Filter>
    </Receiver>
  </Receivers>
Here is not very informative but still a lot referenced msdn documention about it

Couple weeks ago I got curious about the most overlooked element <Data>. Msdn promises me that It contains a string that is used to pass parameters to the receiver.
Ok, fair enough. But I couldn't find a real example why I would need it!
...and I've found!
I have created a AutoPropagateTitleEventReceiver which is responsible for a title propagation after the listitem creation.
A pretty common task in the sharepoint proiects...but I found that every time ppl tend to stick with their own specific class for every listtemplate or even create a SPD (Sharepoint Designer) Workflow!
I am not really against nor specific event handlers, not SPD Wf... but I feel that this task can be perfectly perfom within one common class...if only pass the rule "how the title can be constructed"....
 Wait a minute!   <Data></Data> - that' s perfect for a string based rule\pattern!
So the short story short - I have created the pattern rule  - in my case the an internal name of fields from where the value should be gathered to put it in the title  in my general eventreceiver:
private void setTitle(SPItemEventProperties properties)
        {
          
            StringBuilder title = new StringBuilder();
            SPListItem item = properties.ListItem;
            //pattern ex: <Data>InternalName1#InternalName2</Data> ;

            string titlePattern = properties.ReceiverData;
            string[] columns = titlePattern.Split('#');
          
            foreach (string column in columns)
            {
                if(item.Fields.ContainsField(column))
                {
                    SPFieldLookupValue lookupV =null;
                    string columnValue;
                    try
                    {
                        lookupV = new SPFieldLookupValue(item[column].ToString());
                    }
                    catch
                    { }
                     
                       if (lookupV != null)
                       {
                           columnValue = lookupV.LookupValue;
                       }
                       else
                       {
                           columnValue=item[column].ToString();
                       }


                       title.AppendFormat(" {0}", columnValue);
                }
            }
          
            object titleF =item.Fields["Title"];
            if(!String.Equals(titleF.ToString(),title.ToString()))
            {
                item["Title"] = title.ToString();
                this.DisableEventFiring();               
                item.Update();
                this.EnableEventFiring();
            }
        }

There is only one thing left - add association to the list where I need to autogenerate the title:
<Receivers ListTemplateId="{your listtemplate id}">
    <Receiver>
      <Name>ItemAddedAutoTitleEventHandler</Name>
      <Type>ItemAdded</Type>
      <SequenceNumber>10000</SequenceNumber>
      <Assembly>SE.SharePoint.blog, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a4607a0133bbaeb2</Assembly>
      <Class>SE.SharePoint.blog.EventHandlers.AutoPropagateTitleEventReceiver</Class>
      <Data>InternalColumn1#InternalColumn2</Data>
      <Filter></Filter>
    </Receiver>
  </Receivers>

For every list I want to  have the AutoPropagateTitleEventReceiver  I need to add the element with related ListTemplateId . And at some point probably I will need a specific logic in the receiver for a particular list. No big problem - I can always create an additional specific event handler and add as second; using  the sequence number to control what receiver will go first.

Happy sharepointing!

Monday, January 3, 2011

Schema list and customs forms

Here is a quick note how to implement a custom  form in the schema list:
In the schema file we have 2 sections which are responsible for custom forms:
  <ContentType>
......................
 <XmlDocuments>
          <XmlDocument NamespaceURI="http://schemas.microsoft.com/sharepoint/v3/contenttype/forms">
            <FormUrls xmlns="http://schemas.microsoft.com/sharepoint/v3/contenttype/forms/url">             
              <Display>/Lists/CustomDisplay.aspx</Display>
            </FormUrls>
          </XmlDocument>
        </XmlDocuments>
        <Folder TargetName="Item" />
      </ContentType>
   
    </ContentTypes>
    <Forms>
      <Form Type="DisplayForm" Url="DiscussionView.aspx" WebPartZoneID="LeftBottom" />
      <Form Type="EditForm" Url="EditForm.aspx" WebPartZoneID="LeftBottom" />
      <Form Type="NewForm" Url="NewForm.aspx" WebPartZoneID="LeftBottom" />
<Forms>

It's really worth to notice that section <XmlDocument > has two options: 1. you can set form template id which should be place into the form ex:

        <XmlDocument NamespaceURI="http://schemas.microsoft.com/sharepoint/v3/contenttype/forms">
          <FormTemplates xmlns="http://schemas.microsoft.com/sharepoint/v3/contenttype/forms">
            <Display>ListForm</Display>
            <Edit>ListForm</Edit>
            <New>ListForm</New>
          </FormTemplates>
        </XmlDocument>
      </XmlDocuments>
ListForm is out the box form template which you can find it DefaultTemplate.aspx - msdn reference

2. you can set the page which should be displayed for the content type:
<XmlDocument NamespaceURI="http://schemas.microsoft.com/sharepoint/v3/contenttype/forms">
            <FormUrls xmlns="http://schemas.microsoft.com/sharepoint/v3/contenttype/forms/url">             
              <Display>/Lists/CustomDisplay.aspx</Display>
            </FormUrls>
          </XmlDocument>
        </XmlDocuments>
As you can see the difference between those options is -usage the different element "FormTempates" for form template Id option and FormUrls - msdn reference

The section <Forms>  is responsible for forms for a list, not for a particular content type.

One of the real scenario that I found in my project, developers create a custom form for a list and keep the <FormTemplates> untouched.
To make this approach work you should be aware of how the sharepoint treats these 2 sections in the list. First it looks for form urls and it looks for content type FormTemplate and then it puts the form template from content type to the page which is directed in the Forms section on the Zone which set in the attribute WebPartZoneID for a form. Developers tend to set a fake zone in this attribute. So they trick the sharepoint, it can't find a zone and throws the message during the first display of the page and closes this ListForm Webpart. Voila! - you have a very custom form.
The custom form itself can be with webparts or with controls in it.