Visar inlägg med etikett CQWP. Visa alla inlägg
Visar inlägg med etikett CQWP. Visa alla inlägg

onsdag 24 augusti 2011

More about the upgraded Content Query Web Part

A lot of the previous posts here are related to issues I found when I upgraded a farm from MOSS to SP2010. One of the most common web part used in the main site collection is the Content Query Web Part (CQWB). Unfortunately we've had plenty of strange issues with this web part. Some things that worked fine before the upgrade failed.

Some web parts didn't return any items (All these web parts are predefined in xml and added to sites via onet or manually as separate web parts). I think I've mentioned before that CQWBs scoped to a specific list should use the internal name of the fields specified in most places, but for CQWBs scoped to cross-list queries should use the id of the fields. In MOSS the sorting failed if the sort field was specified with an internal name so that field was specified with the field id, but fields in queries, common viewfields, column renames etc were specified with the field names. After upgrading most of the failing web parts could be fixed by using the correct field specification type, i.e. we exported the web parts, adjusted the xml and replaced the failing ones.

But more issues came up after a while. Some web parts crashed the page when we should edit it via UI. After a lot of struggle I found the failure. In the web part xml, four fields were specified as common view fields. The ULS said that the xml-definition for some fields couldn't be found. Those fields had nothing to do with this, but got me in the correct path. The actual exception that crashed the page was not caused by the web part, but it's toolpart. When the textboxes that displays the common view fields (not available in MOSS) are populated, the field identifiers are retrieved from the web parts xml-definition and fetched as SPField objects to get the display name.

In the MOSS days things worked fine even if the specified common view fields actually existed or not (to be honest I haven't actually looked at the code, but that was the behaviour), but now the fields MUST exist in the correct scope, i.e. in the list if the query is set to a specific list. Using dotPeek I read through loads of sharepoint (2010) code and found that they in one critical place didn't check for a possible null exception.
When the toolpart shall populate the textboxes that displays the DisplayName of the DataMappings or CommonViewFields, it uses the populateFieldsToDisplayControlGroup to loop through the fields specified to get the DisplayName. I haven't looked too deep inside the DataMapppings part of the loop, but when it comes to the CommonViewFields it calls the getDataMappingField method where str1 is the specified field name to get the SPField object and passes it to the ContentByQueryWebPart.GetFieldTitle method to get the DisplayName.

In the getFieldFromCollection method we can se that the first try to get the SPField object is by the Id and the second try is by the internal name. But the method can also return a null if the field doesn't exist in the SPFieldCollection passed from the getDataMappingField method (i.e. SPList.Fields in the case of a single list scope). And so the ContentByQueryWebPart.GetFieldTitle can get a null as an input parameter and it throws a null reference exception when it returns the field.Title value.

    public class ContentByQueryWebPart : CmsDataFormWebPartINamingContainerIWebPartVariationUpdate
    { 
... 
        internal static string GetFieldTitle(SPField field)
        {
            if (field.Id == ContentByQueryWebPartConstants.catchAllId)
                return Resources.GetDocumentManagementString("KeyFiltersKeywordsCatchAll_AllTags");
            else
                return field.Title;
        }
 
        internal static SPField getFieldFromCollection(Guid fieldId, string internalName, SPFieldCollection fields)
        {
            SPField spField = (SPFieldnull;
            try
            {
                if (fieldId.CompareTo(Guid.Empty) != 0)
                    spField = fields[fieldId];
            }
            catch (ArgumentException ex)
            {
            }
            try
            {
                if (spField == null)
                {
                    if (!string.IsNullOrEmpty(internalName))
                        spField = fields.GetFieldByInternalName(internalName);
                }
            }
            catch (ArgumentException ex)
            {
            }
            return spField;
        }
...
    }


    public sealed class ContentByQueryToolPart : ToolPart
    {
...
        private void populateFieldsToDisplayControlGroup(bool clearInvalidMappings)
        {
            ArrayList allSlotNames = this.contentByQueryWebPart.AllSlotNames;
            Hashtable stringAsHashTable1 = ContentByQueryToolPart.getStringAsHashTable(this.contentByQueryWebPart.DataMappings, '|'':');
            Hashtable stringAsHashTable2 = ContentByQueryToolPart.getStringAsHashTable(this.contentByQueryWebPart.CommonViewFields, ';'',');
            int index1 = 0;
            while (index1 < allSlotNames.Count)
            {
                string str1 = allSlotNames[index1] as string;
                string str2 = string.Empty;
                if (stringAsHashTable1.Contains((object) str1))
                {
                    string input = stringAsHashTable1[(object) str1] as string;
                    if (clearInvalidMappings && "LinkUrl".Equals(str1))
                    {
                        StringBuilder dataMappingResult = new StringBuilder();
                        string[] strArray1 = input.Trim().Split(new char[1] {';'});
                        int index2 = 0;
                        while (index2 < strArray1.Length)
                        {
                            string str3 = strArray1[index2].Trim();
                            if (!string.IsNullOrEmpty(str3))
                            {
                                string[] strArray2 = str3.Trim().Split(new char[1] {','});
                                Guid fieldId = Guid.Empty;
                                string internalName = string.Empty;
                                if (strArray2.Length > 1)
                                {
                                    fieldId = new Guid(strArray2[0]);
                                    internalName = strArray2[1];
                                }
                                SPField dataMappingField = this.getDataMappingField(fieldId, internalName, SPContext.Current.Web);
                                if (this.isSafeDataMapping(dataMappingField, str1))
                                    ContentByQueryToolPart.addFieldIntoDataMapping(dataMappingField, dataMappingResult, (StringBuildernull);
                            }
                            checked
                            {
                                ++index2;
                            }
                        }
                        input = ((object) dataMappingResult).ToString();
                    }
                    str2 = this.getDisplayedDataViewMapping(SPContext.Current.Web, input);
                }
                else if (stringAsHashTable2.ContainsKey((object) str1))
                    str2 = ContentByQueryWebPart.GetFieldTitle(this.getDataMappingField(Guid.Empty, str1, SPContext.Current.Web));
                this.fieldsToDisplayTextBoxes[index1].Text = str2;
                checked
                {
                    ++index1;
                }
            }
        }

        private SPField getDataMappingField(Guid fieldId, string internalName, SPWeb web)
        {
            return this.contentByQueryWebPart.CurrentQueryScope() != ContentByQueryWebPart.QueryScope.List ? ContentByQueryWebPart.getFieldFromCollection(fieldId, internalName, web.AvailableFields) : ContentByQueryWebPart.getFieldFromCollection(Guid.Empty, internalName, this.targetList.Fields);
        }
...
    }

My fields does exist as site columns but they doesn't exist in the list, so the solution is to either add the fields to the list or to remove them from the CommonViewFields in the web part.

tisdag 21 juni 2011

Different CQWP rendering behavior when upgrading to SP2010

In the past I started to love and hate the Content Query Web Part since you can use it with audience targeting. In the MOSS days you had a very good control of what was happening and what to render. A rather annoying behavior though was that - even in rather normal situations - the query didn't give you the result you wanted.

My customer wanted to render an audience filtered calendar that showed the upcoming 7 events from [Now] and beyond, not [Today] and beyond. Since there is no such thing as <Now/> that you can use in a caml query, I used <Today/>, increased the row limit and in the item style (ItemStyle.xsl) filtered out the items not matching the rules. But sometimes I still didn't get the 7 events I wanted. I found out that the audiene filtering is not included in the query, but is made on the result of the query. So if I requested 20 items, the audience filtering maybe took away 15 items and the rendering rules in the xsl maybe took away 2. So instead of the expected 7 items I got 3.

The only simple solution to solve this was to increase the row limit even more and it worked fine.


But now the behaviour of the rendering is quite different. In theold days the result was rendered as a <table> but now it's a <ul><li> thing. Better HTML, but it has issues. When you render a table row with only empty cells it doesn't show anything. but an empty <li> in a <ul> renders an empty row. So when I filter out an item in the item style it still renders an empty row since the <li> tag is rendered in ContentQueryMain.xsl.
My solution to this was to set the style attribute in the <li> to display:none;. And how to do that? First the <li> tag must be rendered as a tag, not as a text string that is the case in the default ContentQueryMain.xsl (the $BeginListItem variable). In the OuterTemplate.CallItemTemplate I rendered an <li class="dfwp-item"> tag instead of <xsl:value-of disable-output-escaping="yes" select="$BeginListItem" /> so that the last rendered tag is a fully qualified xml-tag. In my item style, if the item doesn't match the rules, I set the attribute of thea fully qualified xml-tag. In my item style, if the item doesn't match the rules, I set the style attribute of the last rendered xml-tag (i.e. the <li>-tag) with <xsl:attribute name="style">display:none;</xsl:attribute>.

Works beautifully!