Dienstag, 8. März 2016

Adding Custom CSS without touching Masterpage

You can do that using the Delegate-Control mechanism and the builtIn Control "CSSRegistration".

Simply create a Feature (Web- or Sitescoped) and a Module and put the following Control Definition into it:

  
    URLToCSSFile
  
If you start your url without "/", it will assume your CSS-File lies within the "Styles"-Folder in _layouts and prefixes the corresponding parts automatically

Mittwoch, 17. Februar 2016

Adding ECB MenuItem using Javascript

Recently I got the request to view an ECB-Menu Entry only for specific Views. Therefore, the OOTB Version was not a viable option. So I checked out how the ECB-Menu is generated and this is what I ended up with.
It is to how you add this JS to your Web. you can do it using a ContentEditorScript Webpart to a single View, ... . I used a Delegate Control which injected a JS-File to a Web, if the Current-Context was a List and the View was a desired one.


First of all, you have to attach yourself to an Event called "CleanupContextMenu"
like
SP.SOD.delayUntilEventNotified(<funcionName>, "CleanupContextMenu");

"CleanUpContextMenu" is triggered from the ECB-Menu-Builder Function when it is mostly done. So, you can attach to this event and modify it before it is rendered.
The Function you call can look like this
<functionName>: function(menuElement, context) {
            a = {};
            a.Title = "Doing something";
            a.Action = "javascript:doSomeThingWhithItem({ItemId});";
            a.Sequence = 65536;

            var e = ReplaceUrlTokens(a.Action, context);

            v = CIMOpt(menuElement, a.Title, e, null, null, String(a.Sequence));
            v.id = "ID_" + a.Title;

            if (!window.oldBuildMenu) {
                oldBuildMenu = BuildMenu;

                BuildMenu = function (a) {
                    b = oldBuildMenu(a);
                    <functionName>(b, a);
                    return b;
                }
            }
        }

No big magic. The function is called with the parameters of the Menu built so far and the context, with additional Information like the List and View the ECB-Menu is belonging to.
the next thing is to create a object if Properties for "Title", "Action" and "Sequence". I don´t think I have to go further on their purpose.
In the next line I use the builtIn Function to replace the UrlTokens of the Action with current Variables (eg the ItemId is replaced by the real ID of the current Item. With the Function "CIMOpt" a Menu Option is placed into the Menu using your Properties and in the next line the new Menu-Option is equipped with an ID.

Last but not least:
When an Event is triggered, all functions are called which want to be notified of it. But then, all Notification Requests are cleared. So, the next Time when the Event is called, your function won´t be triggered. Attaching to the event again when called will result in an endless loop.
So, I overwrite the old BuildMenu-function with a custom one, which is calling the old function and then my function.

Freitag, 22. August 2014

Check if CurrentUser belongs to a SharpeointGroup due to AD-Groups

Recently I had to check if a User belongs to specific SharePoint-Groups. While SPUser.Groups worked well as long as the user was directyl added to the Groups, it did not work when an Active-Directory Group that the user belongs to was added to a SharePoint-Group.

I first tried by resolving the AD-Groups iteratively, however that was obviously quite slow and expensive. There had to be a better solution and I found it:

As SharePoint uses Claims-Authentication (by Default in 2013) all the AD-Groups (at least their SIDs) are alredy in the users Claims, including their complete hirarchy.

So all you have to do is something like:

var claims = ((Microsoft.IdentityModel.Claims.ClaimsIdentity) testuser.Identity).Claims;
to get the current Users Claims. In this Claims collection you have besides others the current users Groups, represented by their SIDs. You have to reference Microsoft.IdentityModel, which is usually located in GAC on a SP2013 Server.

To check if the user or one of it´s Groups belongs to a specific SharePoint-Group, you simply have to do:
SPSecurity.RunWithElevatedPrivileges(delegate{
   using (var elevatedSite = new SPSite(SPContext.Current.Site.ID))
{
   var web = elevatedSite.RootWeb;
var group = web.SiteGroups.GetByName(groupName);
foreach (SPUser user in group.Users)
{
   if (user.IsDomainGroup)
{
   //var users = new List();
var sid = user.LoginName.Split('|')[1];
if (claims.Any(c => String.Equals(c.Value, sid, StringComparison.CurrentCultureIgnoreCase)))
{
   returnValue = true;
break;
}
} 
   else if (user.LoginName == userName)
{
returnValue = true;
break;
}
}
}
});

Maybe there is a simple way to get all SharePoint-Groups, where one of the SIDs belong to, too. I did not try that yet.

Show only 5 Days in Default-Calendar View (Mo-Fr)

I just had the requirement to Show only Monday to Friday in the Calendar View. The Environment was SharePoint 2010, I only had rights to edit the User Interface.

So I added a CEWP, which is linked to a Textfile located in a doc-Library, containing the following Script. The Script is overwriting some Default-Functions, which are responsible for Rendering. Besides some specific Appointments, starting with a specified String are rendered with additional attributes and it is showing all appointments (no expand/collapse anymore)

The Script is using some functions of (an old verions) of JQuery, because it is already loaded on the site. However, it might not be very difficult, to replace that functions into a native-way, if you don´t have JQuery.

The Content of my script-File:

<script language="javascript">
ExecuteOrDelayUntilScriptLoaded(function() {
//Removing Weekend and adjusting the calculated width of appointments
$('th.ms-acal-week-top').css('width', '20%');
$('th.ms-acal-month-top').css('width', '20%');
$('table.ms-acal-month tr').each(function(index) {$(this).find('td:gt(4), th:gt(5)').remove()});
SP.UI.ApplicationPages.CalendarTableBase.prototype.$3T = function(c,d){
        ULSvSp:;
        for(var b=0,a=0;
        a<d && a < 5;
        a++)b+=this.$1o(c+a);
        return b;
    }

SP.UI.ApplicationPages.CalendarTableBase.prototype.get_$2f = function(){
        ULSvSp:;
        return 5;
};

SP.UI.ApplicationPages.CalendarStateHandler.prototype.onTableSucceed =function(a){
        ULSvSp:;
        this.$D_1.$4V();
        this.$3H_1=false;
        a.Options&2&&this.$D_1.$7s(a.Table);
 $('table.ms-acal-detail tr').each(function(index) {$(this).find('td:gt(4), th:gt(5)').remove()});
        a.Options&8&&a.Options&32&&this.$D_1.$86(a.Dates,a.RangeJDay);
        this.$D_1.$7Z(this.$a_1.get_$2C());
        this.$2c_1=false;
        0===(a.Options&1)&&this.$2A_1()
};
//set all days to "Expanded"
SP.UI.ApplicationPages.CalendarInfoGrid.prototype.$38_0 = true;
//Applying specific background-color for all appointments starting with "Absenz"
SP.UI.ApplicationPages.ItemBuilder.prototype.$4P = function($p0, $p1, $p2, $p3, $p4, $p5, $p6, $p7) {
        var $v_0 = $p1.$c;
        $p0.append('<div class=\"ms-acal-item');
        if (!$v_0.$N.primary) {
            $p0.append(' ms-acal-color');
            $p0.append($v_0.$N.color);
        }
        $p0.append('\" style=\"');
        this.$3j_0($p0, $p2, $p3, $p4, $p5);
        $p0.append('border-width:');
        $p0.append($p6);
 if ($v_0.$3R.match(/^absenz/i))
 {
  $p0.append(';background-color: pink');
 } 
        $p0.append(';\" title=\"');
        this.$4X_0($p0, $v_0);
        $p0.append('\"');
        this.$4D_0($p0, $p1.$2K);
        $p0.append('\">');
        $p0.append('<div class=\"ms-acal-mdiv\">');
        if ($p7) {
            this.$5Q_0($p0, $v_0);
        }
        this.$3N_0($p0, $v_0);
        if (!this.$5_0($v_0.$1G)) {
            $p0.append(' (');
            $p0.append($v_0.$1G);
            $p0.append(')');
        }
        $p0.append('</div></div>');
    }
}, "SP.UI.ApplicationPages.Calendar.js");
</script>

Don´t really know, if it works in SP 2013 too, but as SP2013 is doing the rendering also in JS, it might be not very different to the SP2010 solution

Regards

Montag, 24. Februar 2014

Custom Autocomplete for Searchbox using Search Results

Lately I got an request, to give the search Box an "Autocomplete-Feature". OOTB there is already such a Feature, but it works only with fixed and via Central-Administration uploaded Suggestions or the Users personal "favorites" (Queries, that the user did at least 6 times).

The request was, that the SearchBox should Show available words for Searching, matching the users current entry.

CustomSearchBox.AutoCompleteBehavior.prototype = Object.create(AjaxControlToolkit.AutoCompleteBehavior.prototype); CustomSearchBox.AutoCompleteBehavior.prototype._getSuggestion = function () { try { var clientContext = new SP.ClientContext.get_current(); var keywordQuery = new Microsoft.SharePoint.Client.Search.Query.KeywordQuery(clientContext); So, here is what I did:

The search Box uses the "AjaxControlToolkit.AutoCompleteBehavior", so this is where I started to hook in.

First, I made a custom Class, which basically is a copy of AjaxControlToolkit.AutoCompleteBehavior", and overwrite the method, which is loading the displayed Suggestions: 
Type.registerNamespace('CustomSearchBox');

function initBehavior() {
    CustomSearchBox.AutoCompleteBehavior = function (element) {
        AjaxControlToolkit.AutoCompleteBehavior.initializeBase(this, [element]);
    };

            keywordQuery.set_queryText("*" + this._currentPrefix + "*");
            var searchExecutor = new Microsoft.SharePoint.Client.Search.Query.SearchExecutor(clientContext);
            var results = searchExecutor.executeQuery(keywordQuery);
            clientContext.executeQueryAsync(Function.createDelegate(this,
                function (sender, e) {
                    try {
                        var rows = results.m_value.ResultTables[0].ResultRows;
                        var reg = new RegExp("\\b\\w*" + this._currentPrefix + "\\w*\\b","ig");
                        var regPart = new RegExp("(" + this._currentPrefix + ")", "ig");
                        var words = [];
                        
                        for (var i = 0; i < rows.length; i++) {
                            var row = rows[i];
                            var hhp = row.HitHighlightedSummary.replace(/<(.|\n)*?>/g, '');

                            var match = reg.exec(hhp);
                            if (match) {
                                for (var j = 0; j < match.length; j++) {
                                    {
                                        var bolded = match[j].replace(regPart, "$1");
                                        if (words.indexOf(bolded) == -1)
                                            words.push(bolded);
                                    }
                                }
                            }
                        }

                        var completionItems = new Microsoft.SharePoint.Client.Search.Query.QuerySuggestionResults();

                        completionItems.set_queries(words);
                        completionItems.set_peopleNames([]);
                        completionItems.set_personalResults([]);

                        this._update(this._currentPrefix, completionItems, true);
                    }
                    catch (ex) {
                        Srch.U.trace(null, "CustomSearchBox.AutoCompleteBehavior._update", ex.toString());
                    }
                }),
                function (sender, failureArgs) { });
        }
        catch (e) {
            Srch.U.trace(null, "CustomSearchBox.AutoCompleteBehavior._getSuggestion", e.toString());
        }
        $common.updateFormToRefreshATDeviceBuffer();
    };
    
    CustomSearchBox.AutoCompleteBehavior.registerClass('CustomSearchBox.AutoCompleteBehavior', Sys.UI.Behavior);
    CustomSearchBox.AutoCompleteBehavior.descriptor = AjaxControlToolkit.AutoCompleteBehavior.descriptor;
}

function activateCustomSuggestionBehavior(control) {
    if (control.$3G_3) {
        SP.SOD.executeFunc('sp.js');
        EnsureScriptFunc('ajaxtoolkit.js', 'AjaxControlToolkit.AutoCompleteBehavior', function() {
            if (Srch.U.n(control.$f_3[control.$I_3])) {
                initBehavior();
                var $v_0 = {};
                $v_0['id'] = control.$I_3;
                $v_0['completionInterval'] = control.$2y_3;
                $v_0['completionListElementID'] = control.$I_3;
                $v_0['parentElementID'] = control.$c_3;
                $v_0['completionListCssClass'] = 'ms-qSuggest-list';
                $v_0['completionListItemCssClass'] = 'ms-qSuggest-listItem';
                $v_0['personalResultTitleCssClass'] = 'ms-qSuggest-personalResultTitle';
                $v_0['hrCSSClass'] = 'ms-qSuggest-listSeparator';
                $v_0['enterKeyDownScript'] = '$find(\'' + SP.Utilities.HttpUtility.ecmaScriptStringLiteralEncode(control.get_id()) + '\').search($get(\'' + SP.Utilities.HttpUtility.ecmaScriptStringLiteralEncode(control.$d_3) + '\').value)';
                $v_0['highlightedItemCssClass'] = 'ms-qSuggest-hListItem';
                $v_0['minimumPrefixLength'] = control.$30_3;
                $v_0['offsetWidth'] = -2;
                $v_0['queryCount'] = control.$2z_3;
                $v_0['personalResultCount'] = control.$2w_3;
                $v_0['showPeopleNameSuggestions'] = control.$3F_3;
                $v_0['personalResultTitleSingular'] = Srch.Res.qs_PersonalResultTitleSingular;
                $v_0['personalResultTitlePlural'] = Srch.Res.qs_PersonalResultTitlePlural;
                $v_0['overlappingElementID'] = control.$F_3;
                $v_0['sourceId'] = control.get_querySuggestionsSourceID();
                Srch.U.createBehavior(control.$I_3, CustomSearchBox.AutoCompleteBehavior, $v_0, control.$d_3);
                control.$f_3[control.$I_3] = true;
            }
        });
    }
}
Then I created a custom DisplayTemplate which includes the scriptfile with the above js-code:

    <script>

        $includeScript(this.url, "~sitecollection/Style Library/Custom/JSLibraries/Custom.Search.js");

    </script>


and change the onkeydown function of the corresponding input

  <input type="text" value="_#= $htmlEncode(ctx.ClientControl.get_currentTerm()) =#_" maxlength="2048"

    accesskey="_#= $htmlEncode(Srch.Res.sb_AccessKey) =#_"

    title="_#= $htmlEncode(prompt) =#_"

    id="_#= $htmlEncode(searchBoxId) =#_" autocomplete="off" autocorrect="off"

    onkeypress="if (Srch.U.isEnterKey(String.fromCharCode(event.keyCode))) { $getClientControl(this).search(this.value);return Srch.U.cancelEvent(event); }"

    onkeydown="var ctl = $getClientControl(this); activateCustomSuggestionBehavior(ctl);"

    onfocus="var ctl = $getClientControl(this);ctl.hidePrompt();ctl.setBorder(true);"

    onblur="var ctl = $getClientControl(this);ctl.showPrompt();ctl.setBorder(false);"

    class="_#= inputClass =#_" />


Last but not least I had to assign the new Displaytemplate to the SearchBox, and I had a Autocomplete-Suggestion function using the search.

Anyway, as the sharepoint search is used, you have to keep following in mind:
- Search delivers and orders results based on the current user, so it is possible that the autocomplete delivers different results for different users
- Search delivers results in pages (default 10 items per page), so in the above version, if all of the items on the first page contains the same word, you might get only one word in the autocomplete box. You can change this by changing the pagesize in the query issued by the autocomplete-suggestion function. But keep in mind: the more items you load, the greater is the impact on performance
- If it doesn´t work check the following:
 -- necessary SP-js-files are loaded
 -- ShowPeopleSuggestions is activated in the search-box Webparts properties

Of course you could also do that by putting the whole JS-Code in a Content-Editor-Webpart and overwrite the onkeydown-function there. However, in my opinion the above is the cleanest and most modular solution.

Regards

Freitag, 2. August 2013

Allow multiple of Workflowsubscription on same ListItem

This applies to the new Workflow 2013 Engine:

By default Sharepoint doesn´t allow to have multiple Instances of a Workflow running on the same ListItem.

This gave me headache because I had a Workflow to start when an Item is updated. However, if the Workflow got updated again before the Workflow of the last Update ended, I was not able to start the Workflow again. Even if I canceled the former Workflow it wasn´t possible, because sometimes the cancellation process lasts some seconds and I weren´t able to start a new Workflow, even if the former was in cancelling state. In the End I had no workflow running at all.

However, there is a solution:

If you deploy the Workflow subscription by declarative XML using Visual Studio, you should have somewhere the File "WorkflowStartAssocioation" in the elements.xml of your workflow. If you attach the property:


<Property Name="ManualStartBypassesActivationLimit" Value="1" />

you should be able to start the workflow again (as long as you start the workflow manually by Hand or by Code using EventReceiver).

This works also, if create the Subscription by Code using the SubscriptionService or even update an existing Subscription. There is a Property "ManualStartBypassesActivationLimit" in the Subscription-Object which you can set to true.



Donnerstag, 18. April 2013

Setting the Dataprovider for Resultscriptwebpart programmatically (eg. for setting the QueryTemplate)

For doing that you have to create a instance of the DataProviderScriptWebpart, initialize it with the JsonData from the ResultScriptWebPart. There you can set the Propertiy you need and write the PropertiesJason-Member back to the ResultScriptWebpart.DataProviderJson. And of course save the Webpart finally.


           var resultWebPart =  
             webPartManager.WebParts.Cast<WebPart>()  
                    .FirstOrDefault(  
                      wp => wp is ResultScriptWebPart) as ResultScriptWebPart;  
   
           var querySettings = new DataProviderScriptWebPart  
             {  
               PropertiesJson =  
                 resultWebPart.DataProviderJSON  
             };  
   
           querySettings.Properties["QueryTemplate"] =  
             "{searchboxquery} (contentclass:STS_ListItem OR IsDocument:True)";  
   
           resultWebPart.DataProviderJSON = querySettings.PropertiesJson;  
   
           webPartManager.SaveChanges(resultWebPart);  

But beware: Altough there is a Property DataProviderScriptWebPart.QueryTemplate which you can set, it is not included if you request the DataProviderScriptWebPart.PropertiesJson. You have to set the property using the  Properties-Collection of the object.

(of course you could also simple edit the Json-String itself, but I prefer this way).