CHUVASH.eu

CHunky Universe of Vigouros Astonishing SHarepoint :)

Struggling with Taxonomy in CSOM

The parts of the CSOM for updating Taxonomy fields are really cumbersome. I mean, look at this code, nicely provided by Vadim Gremyshev (@vgrem). To set a value in a taxonomy field we have to assemble a text representation, and adding a “fake” lookup id.

What is needed is a wrapper for handling Taxonomy fields. SPMeta2 and PnP don’t seem to have it yet.

Another issue that I have struggled with today was the missing Microsoft.SharePoint.Client.Taxonomy.dll. If you see this error (set customErrors=”Off” in the Web.config), then you have update the reference in the Visual Studio project:

missingtaxonomy-002

Open Properties for the reference called: Microsoft.SharePoint.Client.Taxonomy and ensure that Copy To Local is set to True:

missingtaxonomy-003

For some reason, this reference added through “App for SharePoint Web Toolkit” nuget package adds a reference to an assembly from your computers GAC.

A new Chuvash keyboard layout

The Chuvash keyboard layout has been the Russian keyboard layout with 4 Chuvash letters that are typed by pressing the right Alt button plus the base letter. Some of the arguments have been

  1. Users don’t need to switch or learn a new keyboard layout. They can keep on typing Russian texts and sometimes Chuvash texts
  2. It is easy to communicate about how the right Alt button works. The Right-Alt-technique is also used in Esperanto, Polish and other languages.
  3. The letters are placed according the labels

Recently two major events happened that made the question about the Chuvash keyboard layout important:

  1. We are working on a Chuvash keyboard for iOS. There we have less place and we have to remove rare Russian letters from the first keyboard screen. There are no physical labels. So we can rethink the whole keyboard.
  2. chuvash.org finally moved from latin equivalents with diacritic marks to Cyrillic letters (Cyrillic extended script). Therefore we need to update users’ keyboard layouts

I’ll write a separate post about the Chuvash Keyboard for iOS. One of the important things we made during that work was to find the frequency of the Chuvash letters. This was used to design the keyboard layout.

Here is the most recent version of the keyboard layout (first screen):

cv-kbd-ios

These are the principles for placing the letters:

  • The most used letters are in the middle.
  • Consonants and vocals come after each other. We tried to avoid many consonants after each other.
  • The letters are often in the same area as in the Russian keyboard layout (but it is not so important)

Now to the physical keyboard

When it is possible on a virtual keyboard, wouldn’t it be worth trying on a physical keyboard? Knowing the “best” layout, we can implement it for a physical keyboard. Let’s do it for xkb. xkb is a keyboard system for Linux. I wrote a few articles on that topic.

Many minority languages in Russian use the Russian keyboard layout plus their Cyrillic letters instead of numbers (Bashkir, Udmurt, Kalmyk) or Right-Alt-combinations (Chuvash, Sakha, Komi…). Two other languages have their own keyboard layouts for primary keys: Tatar and Ossetian. Ossetian language has only one extra letter. The Tatar alphabet contains a few more. Let’s look at the Tatar keyboard layout for xkb:

tatar-xkb-kbd

The Tatar keyboard layout uses their letters on the primary keys and puts the Russian letters in the Right-Alt-combinations. It allows:

  • A quicker typing in Tatar
  • And access to Russian letters, because they are part of the official Tatar alphabet, but they are only used in Russian loanwords. The placement of those rare Russian letters are the same as in the Russian layout (except that they are accessible by pressing the Right-Alt button).

Now the Chuvash keyboard layout for Linux and Windows is as follows:

chuvash-xkb-kbd

When I use it, I always press the Right-Alt, because the ӑӗҫӳ in Chuvash are very common. So the Right-Alt is not an exception, rather that a regular typing behaviour. Some Chuvash frequently used Chuvash letters (х, й, э) are placed too from the middle. Some rare letters (ф, ц, ж, о, г, щ) are too “near”.

So let’s change it. If we just take the keyboard layout designed for iOS and put the rare Russian letters “behind the Right-Alt button”, then we’ll get this:

chuvash-xkb-kbd-2015

This keyboard layout will demand some time to learn, but once learned, it will provide

  • a better and quicker typing in Chuvash,
  • less pain in the right thumb,
  • and, perhaps, less Russian loanwords caused by laziness.

Regarding the learning, it could be facilitated using keyboard stickers, printed for Chuvash keyboards. Here is how Russian stickers look like:

The xkb code for the new Chuvash keyboard layout

// Chuvash Keyboard Layout that is organized according the letter frequency of Chuvash
// Author Anatoly Mironov @mirontoli
// Last changes: 2015-01-03
partial alphanumeric_keys
xkb_symbols "cv" {
    include "ru(winkeys)"

    name[Group1]= "Chuvash";

    key.type[group1]="FOUR_LEVEL";


    key <AD01> {[ U04F3,  U04F2 ]}; // ӳ
    key <AD02> {[ Cyrillic_shorti,  Cyrillic_SHORTI, Cyrillic_tse,     Cyrillic_TSE ]}; // й, ц
    key <AD03> {[ Cyrillic_u,       Cyrillic_U ]}; 
    key <AD04> {[ Cyrillic_ka,      Cyrillic_KA ]}; 
    key <AD05> {[ Cyrillic_ie,      Cyrillic_IE ]}; // е, ё
    key <AD06> {[ Cyrillic_en,      Cyrillic_EN ]}; // 
    key <AD07> {[ U04D7,            U04D6 ]}; // ӗ
    key <AD08> {[ Cyrillic_ha,      Cyrillic_HA ]};
    key <AD09> {[ Cyrillic_sha,     Cyrillic_SHA, Cyrillic_shcha,   Cyrillic_SHCHA ]};
    key <AD10> {[ Cyrillic_ze,      Cyrillic_ZE ]}; 
    key <AD11> {[ Cyrillic_ghe,     Cyrillic_GHE ]};

    key <AC01> {[ Cyrillic_be,      Cyrillic_BE, Cyrillic_ef,      Cyrillic_EF ]}; 
    key <AC02> {[ Cyrillic_yeru,    Cyrillic_YERU ]}; 
    key <AC03> {[ Cyrillic_ve,      Cyrillic_VE ]}; 
    key <AC04> {[ U04D1,            U04D0 ]}; // ӑ
    key <AC05> {[ Cyrillic_el,      Cyrillic_EL ]};
    key <AC06> {[ Cyrillic_a,       Cyrillic_A ]}; 
    key <AC07> {[ Cyrillic_er,      Cyrillic_ER ]}; 
    key <AC08> {[ Cyrillic_o,       Cyrillic_O  ]   };
    key <AC09> {[ Cyrillic_pe,      Cyrillic_PE ]   };
    key <AC10> {[ Cyrillic_e,       Cyrillic_E, Cyrillic_zhe,     Cyrillic_ZHE ]}; 
    key <AC11> {[ Cyrillic_de,      Cyrillic_DE ]};     

    key <AB05> {[ U04AB,            U04AA ]}; // ҫ
    key <AB06> {[ Cyrillic_i,       Cyrillic_I ]};
    key <AB07> {[ Cyrillic_te,      Cyrillic_TE ]}; 
    key <AB08> {[ Cyrillic_softsign,Cyrillic_SOFTSIGN, Cyrillic_hardsign,Cyrillic_HARDSIGN ]};
    key <AB09> {[ Cyrillic_yu,      Cyrillic_YU ]}; 

    include &quot;level3(ralt_switch)&quot;
};

Windows

To create a custom keyboard layout for Windows is easy, but it is hard to contribute to Windows official releases. We only need to install the Microsoft Keyboard Layout Creator.

This is how the new Chuvash Keyboard layout looks like in Windows (Chuvash 2015.1)

chuvash-win-kbd-normal

chuvash-win-kbd-shiftl

chuvash-win-kbd-altgr

Dead keys

Creating a Russian Extended Keyboard Layout

In my spare time I am currently working on a Chuvash-Tatar phrasebook. I have used the Chuvash and Tatar keyboard layout on Linux. They work fine, but switching between them takes time. So I decided to add Tatar letters (right Alt + combinations) to my Chuvash keyboard layout. While adding it I found a combined Russian-Ukranian United keyboard layout and I thought:

  • What if I create a new keyboard layout for Russian that will have almost all additional Cyrillic letters? A Russian Extended keyboard layout could be based on the Russian keyboard layout and have other non-Russian letters.

This is what I have come up to so far. The definition can be found on my project at github: russian-extended-kbd. I will update it more and provide more info about how it is organized and how to install it. I’ll also try to implement it for Windows and maybe for Mac (I doubt it, everything is so locked-down there).

rux-xkb-kbd

This is just a proof-of-concept so far. It only works on Linux (with xkb). Nevertheless, some key characteristics of this layout:

  • It has all the letters of Russian, Erzya, Moksha, Chuvash, Udmurt, Mari (Meadow and Hill Mari), Bashkir, Tatar and other languages of the Russian Federation and other countries.
  • It provides powerful dead keys for (breve, diaeresis, double acute, macron) for composing multiple Cyrillic non-Russian letters
  • It is not as quick as “native” keyboard layouts, but you can type text in many languages without switching the keyboard layout.
  • It has many other characters that are not present in the Russian standard keyboard layout for editing in wiki, markdown and other formats: [ ] { } ~, mathematical symbols: ≈ ÷ ∞ ° ‰ ≤ < > ≥ × •
  • It leaves the numbers. Compared too many other keyboard layouts (see below), this layout does not “steal” the number row. You still can type numbers as usual.

Dead keys

As I mentioned above, dead keys is a powerful feature for composing letters. It is harder to write, but the layout can cover many letters.

These dead keys work

diaeresis ӱ ӥ ӓ ӟ ӝ ӹ ӧ ӵ ӛ ё ї ӫ
double acute ӳ
breve ӑ ӗ ў й
macron ӣ ӯ

These do not work for now (but maybe in future):

cedilla ҫ ҙ
bar ғ ұ
hook ң ҳ қ

So many variants of similar letters

A big challenge in creating a Russian Extended keyboard layout is the fact that languages use different letters for the same sounds (meaning similar sounds).

  • /œ/ is ө (Tatar, Bashkir, Sakha…), and ӧ in (Altay, Udmurt, Mari…)
  • /y/ is ӳ (Chuvash), ӱ (Mari, Altay, Khakas), ү (Tatar, Bashkir, Sakha)
  • /ŋ/ is ҥ (Altay, Sakha, Mari), and ң (Tatar, Bashkir, Khakas, Khanty)

Well, the sounds are not the same, but they are similar. The Swedish Ä is not the same as the German Ä either. If we had a more united Cyrillic script, it would be easier to create a keyboard layout and to read and learn each others’ languages.

The letters from different languages are compare in my Google document.

Some “Native” keyboard layouts of the minority languages of the Russian Federation

chuvash-xkb-kbd

udmurt-xkb-kbd

mari-xkb-kbd

komi-xkb-kbd

kalmyk-xkb-kbd

bashkir-xkb-kbd

ossetian-xkb-kbd

sakha-xkb-kbd

tatar-xkb-kbd

 

 

Other Cyrillic keyboard layouts (outside the Russian Federation)

ukrainian-xkb-kbd

belarusian-xkb-kbd

tajik-xkb-kbd

bulgarian-xkb-kbd

kazakh-xkb-kbd

mongol-xkb-kbd

 

Chuvash localization

Recently I wanted to add Chuvash localization to the jQuery UI datepicker. Unfortunately, my pull request was rejected. The reason is that jQuery UI will be using Globalize framework:

Selection_003

The jQuery Globalize framework relies on CLDR, so

What is Unicode CLDR (Common Locale Data Repository)?

The Unicode CLDR provides key building blocks for software to support the world’s languages, with the largest and most extensive standard repository of locale data available. This data is used by a wide spectrum of companies for their software internationalization and localization, adapting software to the conventions of different languages for such common software tasks

Today there is no Chuvash locale in the CLDR project. So it it is time to add it.

I have filed a ticket on CLDR.

Other Chuvash localization projects

A Chuvash locale exists in a couple of projects:

Code

Just to be complete, here is the Chuvash locale for jQuery UI datepicker that I wanted to add:

/* Written by Anatoly Mironov (@mirontoli). */
(function( factory ) {
	if ( typeof define === &quot;function&quot; &amp;&amp; define.amd ) {

		// AMD. Register as an anonymous module.
		define([ &quot;../datepicker&quot; ], factory );
	} else {

		// Browser globals
		factory( jQuery.datepicker );
	}
}(function( datepicker ) {

datepicker.regional['cv'] = {
	closeText: 'Хуп',
	prevText: '<Кая',
	nextText: 'Мала>',
	currentText: 'Паян',
	monthNames: ['кӑрлач','нарӑс','пуш','ака','ҫу','ҫӗртме',
	'утӑ','ҫурла','авӑн','юпа','чӳк','раштав'],
	monthNamesShort: ['кӑр','нар','пуш','ака','ҫу','ҫӗр',
	'утӑ','ҫур','авн','юпа','чӳк','раш'],
	dayNames: ['вырсарникун','тунтикун','ытларикун','юнкун','кӗҫнерникун','эрнекун','шӑматкун'],
	dayNamesShort: ['выр','тун','ытл','юнк','кӗҫ','эрн','шӑм'],
	dayNamesMin: ['Вр','Тн','Ыт','Юн','Кҫ','Эр','Шм'],
	weekHeader: 'Эрне',
	dateFormat: 'dd.mm.yy',
	firstDay: 1,
	isRTL: false,
	showMonthAfterYear: false,
	yearSuffix: ''};
datepicker.setDefaults(datepicker.regional['cv']);

return datepicker.regional['cv'];

}));

Update multi-value lookup column values in SharePoint 2010 using managed CSOM

Anatoly Mironov:

Reblogging this useful code sample for updating multi-value lookup columns using CSOM in C# in SharePoint 2010, but also valid for SharePoint 2013.

Originally posted on Bin's Dev Notes:

I received a task that needs to update multi-value lookup column value in SharerePoint 2010 using C#.  While it is easy to set columns of simple data types, with lookup column it is a bit more complicated.  Searching Web gives me following link which is helpful. However, that only works with single value column.   After a bit trial and error, I worked out following code that is functioning.

View original

Bypass all custom jslink

bypasscustomjslink-001

Client Side Rendering (CSR) and jslink are great for customizing lists and forms in SharePoint. In my current project we use it a lot of it. A disadvantage of that path, although, is that it might occur javascript errors, during the development phase, but also in production. We do, of course, our best to leverage the best jslink code, but unfortunately we have to live with the fact that errors can occur, especially when we use it for NewForm, EditForm, DisplayForm and View (in list and grid).

If an error occurs, it won’t stop the rest of javascript (it is wrapped in try and catch by SharePoint), but the fields will still not function as intended. It can also be some “corrupt” or old data in the field value that will “break” the jslink code.

I would like to suggest one little fix, an idea I’ve come up to in my jslink-heavy project:

Use a custom url parameter to stop all custom jslink execution.

The query string parameter can be called bypasscustomjslink=true

In every custom jslink, start with this line of code:

That’s it. If you have this in place, you can just manually add this to your url in browser:

?bypasscustomjslink=true

or

&bypasscustomjslink=true

Then all the customized fields and views will be uncustomized until bypasscustomjslink=true is removed. While viewing and editing list items in this uncustomized mode, you can access and repair data as if you never had adjusted it with jslink.

Using this does not mean you can “relax” and start writing crappy code. You still have to produce good code and anticipate all possible errors. bypasscustomjslink is just a convenient “emergency exit” aimed for developers and support to quickly solve problems without needing to reset the JSLink property on fields and list views.

Client Side Rendering with Async dependencies

Yesterday I asked a question on SharePoint StackExchange:

I also asked Elio Struyf on Twitter:

Good idea, Elio Struyf! Now I want to try it out.

Preparations

In this case I’ll be using my example from my blog post yesterday:

Drag and Drop Image using Client Side Rendering

I have created a new list and added a lookup field to my previous list. What I get is a Title of the lookup item, but not my custom field called DragAndDrop. In my test I will try to load the DragAndDrop Image using an ajax call and rendering it after Client Side Rendering is done with my item.

To be complete, I want to show some screenshots for my lookup field:

csr-async-001csr-async-002

It will result in this OOTB rendering:
csr-async-003

Trying out CSR with async dependencies

While working with jslink, first of all I want to show a loading image instead of an empty html element, to show that something is loading:
csr-async-004

Here is the skeletton of the jslink file:

(function () {
    'use strict';

    var display = function (ctx, field) {
        var containerId = &amp;quot;tolle&amp;quot;;
        var loadingImg = _spPageContextInfo.webAbsoluteUrl + &amp;quot;/_layouts/images/loadingcirclests16.gif&amp;quot;;
        //unfortunately SP.Utilities.Utility is not defined at this stage
        //SP.Utilities.Utility.getImageUrl(&amp;quot;loadingcirclests16.gif&amp;quot;);       
        return ['&amp;lt;div id=&amp;quot;', containerId, '&amp;quot;&amp;gt;&amp;lt;img src=&amp;quot;', loadingImg, '&amp;quot;/&amp;gt;&amp;lt;/div&amp;gt;'].join('');
    };

    var overrideContext = {};
    overrideContext.Templates = overrideContext.Templates || {};
    overrideContext.Templates = overrideContext.Templates || {};
    overrideContext.Templates.Fields = {
        'TolleLookup': {
            'DisplayForm': display
        }
    };

    SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideContext);
})();

Making an ajax call

Next step is to initiate an ajax call. I am trying to avoid the jQuery dependency. There is so much you can do with the built-in javascript functions. For making an ajax call I am using SP.RequestExecutor.js

Here is the result:

csr-async-005

The code should be quite self explaining:

(function () {
    'use strict';

    var onDataRetrieved = function(response) {
        console.log(&amp;quot;yippie&amp;quot;);
        var data = JSON.parse(response.body);
        var imgSrc = data.d.DragAndDropImage;
        var container = document.getElementById(&amp;quot;tolle&amp;quot;);
        container.innerHTML = ['&amp;lt;img src=&amp;quot;', imgSrc, '&amp;quot;/&amp;gt;'].join('');
    }

    var onError = function(response) {
        console.error(&amp;quot;failed&amp;quot;, response);
    }
    var initiateAjaxCall = function(ctx) {
        var item = ctx.CurrentItem;
        var fieldName = ctx.CurrentFieldSchema.Name;
        var fieldValue = item[fieldName];
        
        var itemId = fieldValue.split(&amp;quot;;#&amp;quot;)[0];
        var lookupListId = ctx.ListSchema.Field[0].LookupListId;
        var url = [window._spPageContextInfo.webAbsoluteUrl,
            &amp;quot;/_api/web/lists/getbyid('&amp;quot;,
            lookupListId,
            &amp;quot;')/Items(&amp;quot;,
            itemId, &amp;quot;)?$select=DragAndDropImage&amp;quot;].join(&amp;quot;&amp;quot;);
        SP.SOD.registerSod('sp.requestexecutor.js', '/_layouts/15/sp.requestexecutor.js');
        SP.SOD.executeFunc(&amp;quot;sp.requestexecutor.js&amp;quot;, &amp;quot;SP.RequestExecutor&amp;quot;, function () {
            var executor = new SP.RequestExecutor(window._spPageContextInfo.webAbsoluteUrl);
            executor.executeAsync(
                {
                    url: url,
                    method: &amp;quot;GET&amp;quot;,
                    headers: { &amp;quot;Accept&amp;quot;: &amp;quot;application/json; odata=verbose&amp;quot; },
                    success: onDataRetrieved,
                    error: onError
                }
            );
        });
    }
    var display = function (ctx, field) {
        if (!ctx.CurrentItem[ctx.CurrentFieldSchema.Name]) { //if there is no value
            return "";
        }
        var containerId = &amp;quot;tolle&amp;quot;;
        var loadingImg = window._spPageContextInfo.webAbsoluteUrl + &amp;quot;/_layouts/images/loadingcirclests16.gif&amp;quot;;
        initiateAjaxCall(ctx);
        //unfortunately SP.Utilities.Utility is not defined at this stage
        //SP.Utilities.Utility.getImageUrl(&amp;quot;loadingcirclests16.gif&amp;quot;);       
        return ['&amp;lt;div id=&amp;quot;', containerId, '&amp;quot;&amp;gt;&amp;lt;img src=&amp;quot;', loadingImg, '&amp;quot;/&amp;gt;&amp;lt;/div&amp;gt;'].join('');
    };

    var overrideContext = {};
    overrideContext.Templates = overrideContext.Templates || {};
    overrideContext.Templates = overrideContext.Templates || {};
    overrideContext.Templates.Fields = {
        'TolleLookup': {
            'DisplayForm': display
        }
    };

    SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideContext);
})();

Next steps and further considerations

It seems to work, although I have some considerations:

  1. What happens if the data from the ajax call is retrieved before Client Side Rendering is done (then the container is not rendered yet). Even if the risk for that is low, it should be handled properly.
  2. It would be good to have a consistent look and feel in the Display form and in the list view. To make it possible, following should be done:
    1. We must create references to html elements (containers) with unique ids, need to implement logic for generating ids and keeping track of the right elements.
    2. Ajax calls should be bundled, otherwise it will hugely impact the performance, even in a list view with 30 items.

Drag and Drop Image using Client Side Rendering

I continue my series about Client Side Rendering (CSR) and jsgrid. Today I want to try a custom field where users can drag and drop images. The inspiration comes from:

What I want to achieve is:

  1. A custom field that is rendered with jslink
  2. Users can drag and drop small pictures (thumbnails) into the field
  3. A base64 image representation is saved as the field value
  4. Optionally implement pasting images using Clipboard API

Step 1 Create a field with a custom jslink

Create a field of type Note. I am using the PnP Core Extensions to make it quickier:

dnd-001

My jslink file is very simple to begin with:

(function () {
    'use strict';
    function view(ctx, field) {
        return "hello";
    }

    var overrideContext = {};

    overrideContext.Templates = overrideContext.Templates || {};
    overrideContext.Templates.Fields = {
        'DragAndDropImage': {
            'View': view,
            'DisplayForm': view
            //'EditForm': verySimpleNewAndEdit,
            //'NewForm': verySimpleNewAndEdit
        }
    };

    SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideContext);
})();

This will result in the following display form. Just outputting “hello” indicates that my field is jslink are registered correctly:

dnd-002

Step 2. Ensure base64 works in the field

Not all fields will work. I have tried many of them and found that the field type Note with Plain text works for saving base64 images.

First download some sample icons at flaticons.com

dnd-003

Convert an icon to a base64 image using dataurl.net:

dnd-004

Save the string into the Drag And Drop Image field:

dnd-005

Now update the view template in the CSR overrides:

//http://stackoverflow.com/a/822486/632117
//in display form we have to strip the html elements that are pasted in by SharePoint
function strip(html) {
    var tmp = document.createElement("DIV");
    tmp.innerHTML = html;
    return tmp.textContent || tmp.innerText || '';
}
function renderImage(fieldValue) {
    if (fieldValue) {
        return ['<img style="max-width:100px;" src="',
            strip(fieldValue), '"/>'].join('');
    }
    return '';
}

function view(ctx, field) {
    //disable editing in Quick Edit mode
    if (ctx.inGridMode) {
        field.AllowGridEditing = false;
    }
    var fieldValue = ctx.CurrentItem[ctx.CurrentFieldSchema.Name];
    return renderImage(fieldValue);
}

Well, it works:

dnd-006dnd-007

 Step 3. Implement drag and drop

Let’s start with the edit template:

function renderDropZone(fieldValue) {
    var img = renderImage(fieldValue);
    return ['<div id="drop_zone">', img, '</div>'].join('');
}

function edit(ctx, field) {
    var formCtx = SPClientTemplates.Utility.GetFormContextForCurrentField(ctx);
    formCtx.registerGetValueCallback(formCtx.fieldName, function () {
        //will implement the logic later
        return '';
    });
    var fieldValue = ctx.CurrentItem[ctx.CurrentFieldSchema.Name];
    return renderDropZone(fieldValue);
}

var overrideContext = {};
overrideContext.Templates = overrideContext.Templates || {};
overrideContext.Templates.Fields = {
    'DragAndDropImage': {
        'View': view,
        'DisplayForm': view,
        'EditForm': edit,
        'NewForm': edit
    }
};

I have wrapped the image element with a div (id=drop_zone). Now I need some css to show it:

var additionalStyle = [
    '<style>',
    '#drop_zone { height: 100px; width:100px; background: #efe}',
    '</style>'
].join('');
document.write(additionalStyle);

Now there is a clear “drop zone”:
dnd-008
To make it as simple as possible, let’s follow oroboto’s drag and drop code. Here we go:

var processFiles = function (event) {
    var element = event.target;
    event.stopPropagation();
    event.preventDefault();
    removeDropZoneClass(event);

    // FileList object of File objects
    var files = event.dataTransfer.files;
    for (var i = 0, f; f = files[i]; i++) {
        var reader = new FileReader();

        // closure to capture file info
        reader.onload = (function (file, index) {
            return function (e) {
                var dataUri = e.target.result;
                var img = renderImage(dataUri);
                element.innerHTML = img;
            };
        })(f, i);

        // read file as data URI
        reader.readAsDataURL(f);
    }

};

var highlightDropZone = function (e) {
    e.stopPropagation();
    e.preventDefault();
    document.getElementById('drop_zone').classList.add('highlight');
}

var removeDropZoneClass = function (e) {
    document.getElementById('drop_zone').classList.remove('highlight');
}

function prepareDropZone(ctx) {
    if (ctx.inGridMode) {
        return;
    }
    // add event listeners if File API is supported
    var dropZone = document.getElementById('drop_zone');
    if (window.File &amp;amp;&amp;amp; window.FileReader &amp;amp;&amp;amp; window.FileList &amp;amp;&amp;amp; window.Blob) {
        dropZone.addEventListener('drop', processFiles, false);
        dropZone.addEventListener('dragover', highlightDropZone, false);
        dropZone.addEventListener('dragenter', highlightDropZone, false);
        dropZone.addEventListener('dragleave', removeDropZoneClass, false);
    } else {
        dropZone.innerHTML = 'The File APIs are not fully supported in this browser.';
        dropZone.className = 'highlight';
    }

};

// ... code omitted for brevity

overrideContext.Templates.OnPostRender = prepareDropZone;

That’s not bad, not bad:

dnd-009

The last thing in this step is to save the base64 value

formCtx.registerGetValueCallback(formCtx.fieldName, function () {
    var dropZone = document.getElementById('drop_zone');
    var img = dropZone.getElementsByTagName('img');
    return img.length ? img[0].src : '';
});

Step 4 Support pasting images

Drag And Drop is cool. But what about pasting images. Wouldn’t it be nice? The functionality is called Clipboard API. You can check which browsers support this on caniuse.com. The simplest example I’ve found is on stackoverflow: How does onpaste work

The code I use is as follows:

document.onpaste = function (event) {
    var items = (event.clipboardData || event.originalEvent.clipboardData).items;
    console.log(JSON.stringify(items)); // will give you the mime types
    var blob = items[0].getAsFile();
    var reader = new FileReader();
    reader.onload = function (e) {
        var dataUri = e.target.result;
        var img = renderImage(dataUri);
        dropZone.innerHTML = img;
    }; // data url!
    reader.readAsDataURL(blob);
}

Here is how pasting works:
dnd-011

Step 5 Error handling (Not done yet)

I need to ensure that

  • only images are allowed
  • only small images are allowed, otherwise base64 will be to heavy
  • proper handling of old browsers, we don’t need to support them, but the users should get good information about what they need to make it work
  • a help icon is shown, when clicked a help callout is shown about how it works

Step 6 Support Quick Edit (Not done yet)

We need code that works in Quick Edit:

  • need to write code for jsgrid
  • need handling of multiple drop zones on the same page (unique ids and smart handling of pasting)

Summary

This post is about discovering what’s possible to do with client side rendering and using today’s technologies like html5 and new javascript apis. I hope you’ve got some inspiration how normal sharepoint fields combined with Client Side Rendering (CSR) api can create more value in your project, in Office 365 and in SharePoint On Premises. Please leave feedback in comments.

Fulll source code

And finally, here comes the most up-to-date full source code for this custom Drag And Drop Image field:

(function () {
    'use strict';
    var additionalStyle = [
        '<style>',
        '#drop_zone { height: 100px; width:100px; background: #efe}',
        '#drop_zone.highlight {background:#fee;}',
        '</style>'
    ].join('');
    document.write(additionalStyle);


    var processFiles = function (event) {
        var element = event.target;
        event.stopPropagation();
        event.preventDefault();
        removeDropZoneClass(event);

        // FileList object of File objects
        var files = event.dataTransfer.files;
        for (var i = 0, f; f = files[i]; i++) {
            var reader = new FileReader();

            // closure to capture file info
            reader.onload = (function (file, index) {
                return function (e) {
                    var dataUri = e.target.result;
                    var img = renderImage(dataUri);
                    element.innerHTML = img;
                };
            })(f, i);

            // read file as data URI
            reader.readAsDataURL(f);
        }

    };

    var highlightDropZone = function (e) {
        e.stopPropagation();
        e.preventDefault();
        document.getElementById('drop_zone').classList.add('highlight');
    }

    var removeDropZoneClass = function (e) {
        document.getElementById('drop_zone').classList.remove('highlight');
    }

    function prepareDropZone(ctx) {
        if (ctx.inGridMode) {
            return;
        }
        // add event listeners if File API is supported
        var dropZone = document.getElementById('drop_zone');
        if (window.File &amp;amp;&amp;amp; window.FileReader &amp;amp;&amp;amp; window.FileList &amp;amp;&amp;amp; window.Blob) {
            dropZone.addEventListener('drop', processFiles, false);
            dropZone.addEventListener('dragover', highlightDropZone, false);
            dropZone.addEventListener('dragenter', highlightDropZone, false);
            dropZone.addEventListener('dragleave', removeDropZoneClass, false);
        } else {
            dropZone.innerHTML = 'The File APIs are not fully supported in this browser.';
            dropZone.className = 'highlight';
        }

        document.onpaste = function (event) {
            var items = (event.clipboardData 
                 || event.originalEvent.clipboardData).items;
            console.log(JSON.stringify(items)); // will give you the mime types
            var blob = items[0].getAsFile();
            var reader = new FileReader();
            reader.onload = function (e) {
                var dataUri = e.target.result;
                var img = renderImage(dataUri);
                dropZone.innerHTML = img;
            }; // data url!
            reader.readAsDataURL(blob);
        }
       
    };

    //http://stackoverflow.com/a/822486/632117
    //in display form we have 
    //to strip the html elements that are pasted in by SharePoint
    function strip(html) {
        var tmp = document.createElement('DIV');
        tmp.innerHTML = html;
        return tmp.textContent || tmp.innerText || '';
    }
    function renderImage(fieldValue) {
        if (fieldValue) {
            return ['<img style="max-width:100px;" src="',
                strip(fieldValue), '"/>'].join('');
        }
        return '';
    }
    
    function view(ctx, field) {
        //disable editing in Quick Edit mode
        if (ctx.inGridMode) {
            field.AllowGridEditing = false;
        }
        var fieldValue = ctx.CurrentItem[ctx.CurrentFieldSchema.Name];
        return renderImage(fieldValue);
    }
    
    function renderDropZone(fieldValue) {
        var img = renderImage(fieldValue);
        return ['Drop files here<div id="drop_zone"', img, '</div>'].join('');
    }

    function edit(ctx, field) {
        var formCtx = SPClientTemplates.Utility.GetFormContextForCurrentField(ctx);
        formCtx.registerGetValueCallback(formCtx.fieldName, function () {
            var dropZone = document.getElementById('drop_zone');
            var img = dropZone.getElementsByTagName('img');
            return img.length ? img[0].src : '';
        });
        var fieldValue = ctx.CurrentItem[ctx.CurrentFieldSchema.Name];
        return renderDropZone(fieldValue);
    }

    var overrideContext = {};
    overrideContext.Templates = overrideContext.Templates || {};
    overrideContext.Templates.Fields = {
        'DragAndDropImage': {
            'View': view,
            'DisplayForm': view,
            'EditForm': edit,
            'NewForm': edit
        }
    };

    overrideContext.Templates.OnPostRender = prepareDropZone;

    SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideContext);
})();

Disabling a column in Quick Edit

jsgrid-015
In my project I have a column called Request Status. This column is not shown in any forms, meaning users should not edit, because it is controlled through the app. Nevertheless it is editable in the Quick Edit.

Yesterday I wrote about jsgrid in my blog. Now comes more. Today I’ll share a little practical solution how one can disable editing a field in Quick Edit.

The field is edited in jsgrid, but to disable it, we only have set the property called AllowGridEditing to false on our column (not even touching the heavy jsgrid api). We can do in the OnPreRender event in our Client Side Rendering (CSR) registration. Having the context object we have access to the Fields (ContextInfo.ListSchema.Field):

(function () {
    var overrideContext = {};
    overrideContext.Templates = overrideContext.Templates || {};
    overrideContext.Templates.OnPreRender = function(ctx) {
    	var statusField = ctx.ListSchema.Field.filter(function(f) {
    		return f.Name === 'Request_x0020_Status';
    	});
    	if (statusField) {
    		statusField[0].AllowGridEditing = false;
    	}
    }
    SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideContext);
})();

Another way is to implement the display form:

(function () {
    var view = function (ctx, field) {
        if (ctx.inGridMode) {
            field.AllowGridEditing = false;
        }
        return window.RenderFieldValueDefault(ctx);
    };
    var overrideContext = {};
    overrideContext.Templates = overrideContext.Templates || {};
    overrideContext.Templates.Fields = {
        'Request_x0020_Status': {
            'View': view
        }
    };
    SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideContext);
})();

JSGrid Basics

JSGrid is the javascript framework in SharePoint used in Quick Edit View (previously Datasheet View). There are a few very good blog posts on this topic (See below in “Sources”). Nevertheless the fact is that jsgrid and working with quick edit from a developer’s perspective is a huge undiscovered area. Articles I have seen are intended for advanced developers. The goal with my post today is to outline the very basics of working with JSGrid. When you know the basics you will be more comfortable to discover and try out more.

The example I want to show is a jsgrid code for a “VerySimpleColumn”. The source code can be found on gist.github.com: https://gist.github.com/mirontoli/838d60df76107fac56a0 To focus on jsgrid, I assume you have knowledge and some experience of working with jslink, which is related to jsgrid.

A word of caution before we start

JSGrid is an undocumented part of SharePoint javascript “ecosystem”, neither it is a part of the official SharePoint javascript api. So actually we should not use it. On the other hand JSGrid indicates something that is more like a full-fledged javascript api because:

  1. It seems to be carefully prepared (all possible situations are covered)
  2. The api is human-readable. The events and properties are called OnBeginEdit, OnEndEdit, Focus, BindToCell and so on (opposed to the properties in the SP.Microfeed.js like $v_1, $v_2)
  3. It follows many best-practices for handling the UI in javascript, eventhough some constructs are clumsy, e.g. using absolute-positioned overlays on top of original table cells while editing a cell value.

So to me it seems quite okay to use it in real applications, but we have to live with the risk that the jsgrid api will be changed without any notice to us developers.

Set up a site column with a custom jslink

The first step is to set up everything so we can start discovering jsgrid. It is just a walkthrough, not a detailed explanation. If you are eager to look at jsgrid, jump directly to that section. In this example we’ll apply a jslink to a custom site column. The very same jslink will be used in jsgrid, too.

First of all add a new Site Column, call it VerySimpleColumn. The type is Multiple lines of text:

jsgrid-001

Let us put it in a new group: Tolle Columns (beautiful, huh?)

jsgrid-002

 

Three lines of plain text. Nothing extravagant.

jsgrid-003

After that we can verify that the column exists. Fine.

jsgrid-004

 

Now we have to update the jslink property for the new column. Make it in an app:

jsgrid-005

Now add a custom list, call it “TolleList”:

jsgrid-006

 

In the List Settings add the new site column, then you’ll see this:

jsgrid-007

 

Now upload an empty js file to the Style Library, call it field.jslink.verysimplecolumn.js:

jsgrid-008

 

It goes so fast :) Now it is time to implement some jslink code

Writing jslink

I’ve been thinking a while. What example could I use for that jslink? I want it to be a very simple example, so we don’t need to concentrate on actual rendering logic. You can see sophisticated examples that I listed below in “Sources”. Here we will be using this example: We’ll append a text to the field value: “This field is fully controlled by jslink”. This text should be visible on all forms, but it is not a part of the actual field value. Well here it is:

jsgrid-009

The code should appear simple for us who have written at least some jslink code.

(function () {
    function verySimpleView(ctx, field) {
        var wrapper = 'This field is fully controlled by jslink<hr><span>{0}</span>';
        var value = ctx.CurrentItem[ctx.CurrentFieldSchema.Name] || "";
        return String.format(wrapper, value);
    }
    
    function verySimpleNewAndEdit(ctx) {
        var wrapper = 'This field is fully controlled by jslink<br><input type="text" id="{0}" value"{1}"/>';
        var id = 'tolle-' + new Date().getTime();
        var formCtx = SPClientTemplates.Utility.GetFormContextForCurrentField(ctx);
 
        formCtx.registerGetValueCallback(formCtx.fieldName, function () {
            var input = document.getElementById(id);
            return input.value;
        });
        var value = ctx.CurrentItem[ctx.CurrentFieldSchema.Name] || "";
        var html = String.format(wrapper, id, value);
        return html;
    }
    var overrideContext = {};
 
    overrideContext.Templates = overrideContext.Templates || {};
    overrideContext.Templates.Fields = {
        'VerySimpleColumn': {
            'View': verySimpleView,
            'DisplayForm': verySimpleView,
            'EditForm': verySimpleNewAndEdit,
            'NewForm': verySimpleNewAndEdit
        }
    };
 
    SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideContext);
})();

This is how it looks like in the DispForm:

jsgrid-010

JSGrid. First Step: Disable the field in Quick Edit

Now we are ready to handle the Quick Edit. What we’ll do first is to disable editing our column. Why? Let’s say we have some complicated logic for rendering and editing of our field, we don’t the quick edit override it. So before we start implementing the QuickEdit part of the field, we want to disable it. Here, perhaps, ends the implementation of Quick Edit for some business cases.

jsgrid-011

To determine the Quick Edit we can use the propery of ther Render Context called inGridMode. The Quick Edit list uses the “View” template:

function handleGridMode(ctx, field) {
    field.AllowGridEditing = false;
}
function verySimpleView(ctx, field) {
    var wrapper = 'This field is fully controlled by jslink<hr><span>{0}</span>';
    var value = ctx.CurrentItem[ctx.CurrentFieldSchema.Name] || "";
    if (ctx.inGridMode) {
        handleGridMode(ctx, field);
    }
    return String.format(wrapper, value);
}

Wiring up a jsgrid EditControl

When our code works in all forms and the values are not “damaged” in the Quick Edit View, then the next step is to create an Edit Control that is used by the jsgrid “engine”. The very minimal Edit Control object is as follows. It contains a few event listeners and properties:

{
    SupportedWriteMode: SP.JsGrid.EditActorWriteType.LocalizedOnly,
    SupportedReadMode: SP.JsGrid.EditActorReadType.LocalizedOnly,
    BindToCell: function () { console.log("tolle BindToCell"); },
    OnCellMove: function () { console.log("tolle OnCellMove"); },
    Focus: function () { console.log("tolle Focus"); },
    OnBeginEdit: function () { console.log("tolle OnBeginEdit"); },
    OnEndEdit: function () { console.log("tolle OnEndEdit"); },
    Unbind: function () { console.log("tolle Unbind"); },
    Dispose: function () { console.log("tolle Dispose"); }
}

This edit control is created and returned in a function that is called: createVerySimpleColumnGridEditControl.

var createVerySimpleColumnGridEditControl = function (gridContext, cellControl) {
    return {
        SupportedWriteMode: SP.JsGrid.EditActorWriteType.LocalizedOnly,
        SupportedReadMode: SP.JsGrid.EditActorReadType.LocalizedOnly,
        BindToCell: function () { console.log("tolle BindToCell"); },
        OnCellMove: function () { console.log("tolle OnCellMove"); },
        Focus: function () { console.log("tolle Focus"); },
        OnBeginEdit: function () { console.log("tolle OnBeginEdit"); },
        OnEndEdit: function () { console.log("tolle OnEndEdit"); },
        Unbind: function () { console.log("tolle Unbind"); },
        Dispose: function () { console.log("tolle Dispose"); }
    }
};

Now we have to register our Edit Control when jsgrid is ready. We have to write a callback function and invoke SP.GanttControl.WaitForGanttCreation. Inside the callback function we register our Edit Control for our field: SP.JsGrid.PropertyType.Utils.RegisterEditControl

function handleGridMode(ctx, field) {
    window.SP.SOD.executeOrDelayUntilScriptLoaded(function () {
        window.SP.GanttControl.WaitForGanttCreation(function (ganttChart) {
            var verySimpleColumn = null;
            var editId = "EDIT_TOLLE_VERYSIMPLEFIELD";
            var columns = ganttChart.get_Columns();

            for (var i = 0; i < columns.length; i++) {
                if (columns[i].columnKey == "VerySimpleColumn") {
                    verySimpleColumn = columns[i];
                    break;
                }
            }
            if (verySimpleColumn) {
                verySimpleColumn.fnGetEditControlName = function (record, fieldKey) {
                    return editId;
                };

                window.SP.JsGrid.PropertyType.Utils.RegisterEditControl(editId, function (gridContext, cellControl) {
                    return createVerySimpleColumnGridEditControl(gridContext, cellControl);
                }, []);
            }

        });
    }, "spgantt.js");
}

In our first version of the Edit Control we only log the event names to the web browser console. It works.

jsgrid-012

Implementing the actual editing

When working on this I did small changes to these events and tried it out in the web browser. I’d recommend it to you, too. Trying out is the best way of learning. Here comes the functions that we need to have to implement:

  • createVerySimpleColumnGridEditControl (“constructor”). Here we initialize the edit control
  • bindToCell. Here we get the cellContext that we save as a “private” variable on that Edit Control object. The cell context is needed to get and set the field value.
  • focus. Here we define what element should be focused. In this case we forward the focus event to the actual input
  • onBeginEdit. Here we show the Edit Control and make it editable.
  • onEndEdit. Here we save the field value and hide the edit control.

jsgrid-013

Some notes on the Edit Control and Events

The Edit Control has a “container” – a html element that contains the representation of the field in edit mode. The Edit Control Container is an overlay – an absolutely-positioned element that exists outside the actual listview. We must set the position and we are responsible for hiding it when the field is not edited:

container.style.cssText = 'visibility:hidden;position:absolute;top:0;left:0;background:#ffffff;';

We also need to set the dimensions of the edit control container. It is quite easy using the information from cellContext:

var bindToCell = function(ctx) {
    cellContext = ctx;
    //An input is put as an overlay. 
    //We have to set the width and height so that it takes the whole cell place
    container.style.minWidth = cellContext.cellWidth + 'px';
    container.style.width = cellContext.cellWidth + 'px';
    container.style.height = cellContext.cellHeight + 'px';
    console.log("tolle BindToCell");
};

In OnBeginEdit we have to show the container, and in OnEndEdit we have to hide it

//OnBeginEdit
cellContext.Show(container);
//OnEndEdit
cellContext.Hide(container);

We have to save the value using the cellContext.

var value = input.value;
cellContext.SetCurrentValue({
    localized: value
});

Final code

(function () {
    var editWrapper = '<span>This field is fully controlled by jslink</span><br><input type="text" id="{0}" value"{1}"/>';
    var createVerySimpleColumnGridEditControl = function (gridContext, gridTextInputElement) {
        var cellContext, inEdit, html, container, input, id;
        id = "tolle-" + new Date().getTime();
        html = String.format(editWrapper, id, "");
        container = document.createElement("div");
        container.innerHTML = html;
        input = container.getElementsByTagName("input")[0];
        container.style.cssText = 'visibility:hidden;position:absolute;top:0;left:0;background:#ffffff;';
        gridContext.parentNode.appendChild(container);
        var bindToCell = function(ctx) {
            cellContext = ctx;
            //An input is put as an overlay. We have to set the width and height so that it takes the whole cell place
            container.style.minWidth = cellContext.cellWidth + 'px';
            container.style.width = cellContext.cellWidth + 'px';
            container.style.height = cellContext.cellHeight + 'px';
            console.log("tolle BindToCell");
        };
        var onCellMove = function () { console.log("tolle OnCellMove"); };
        var focus = function(eventInfo) {
            input.focus();
            console.log("tolle Focus", eventInfo);
        };
        var onBeginEdit = function (eventInfo) {
            inEdit = true;
            var currentValue = cellContext.originalValue.localized;
            if (currentValue) {
                input.value = currentValue;
            }
            cellContext.Show(container);
            console.log("tolle OnBeginEdit");
            this.Focus(eventInfo);
        };
        var onEndEdit = function() {
            cellContext.Hide(container);
            inEdit = false;
            var value = input.value;
            cellContext.SetCurrentValue({
                localized: value
            });
            console.log("tolle OnEndEdit");
        };

        var unbind = function() { console.log("tolle Unbind"); };
        var dispose = function () { console.log("tolle Dispose"); }

        return {
            SupportedWriteMode: window.SP.JsGrid.EditActorWriteType.LocalizedOnly,
            SupportedReadMode: window.SP.JsGrid.EditActorReadType.LocalizedOnly,
            BindToCell: bindToCell,
            OnCellMove: onCellMove,
            Focus: focus,
            OnBeginEdit: onBeginEdit,
            OnEndEdit: onEndEdit,
            Unbind: unbind,
            Dispose: dispose
        }
    };

    function handleGridMode(ctx, field) {
        window.SP.SOD.executeOrDelayUntilScriptLoaded(function () {
            window.SP.GanttControl.WaitForGanttCreation(function (ganttChart) {
                var verySimpleColumn = null;
                var editId = "EDIT_TOLLE_VERYSIMPLEFIELD";
                var columns = ganttChart.get_Columns();

                for (var i = 0; i < columns.length; i++) {
                    if (columns[i].columnKey == "VerySimpleColumn") {
                        verySimpleColumn = columns[i];
                        break;
                    }
                }
                if (verySimpleColumn) {
                    verySimpleColumn.fnGetEditControlName = function (record, fieldKey) {
                        return editId;
                    };

                    window.SP.JsGrid.PropertyType.Utils.RegisterEditControl(editId, function (gridContext, cellControl) {
                        return createVerySimpleColumnGridEditControl(gridContext, cellControl);
                    }, []);
                }

            });
        }, "spgantt.js");
    }


    function verySimpleView(ctx, field) {
        var wrapper = 'This field is fully controlled by jslink<hr><span>{0}</span>';
        var value = ctx.CurrentItem[ctx.CurrentFieldSchema.Name] || "";
        if (ctx.inGridMode) {
            handleGridMode(ctx, field);
        }
        return String.format(wrapper, value);
    }
    
    function verySimpleNewAndEdit(ctx) {
        var id = 'tolle-' + new Date().getTime();
        var formCtx = window.SPClientTemplates.Utility.GetFormContextForCurrentField(ctx);

        formCtx.registerGetValueCallback(formCtx.fieldName, function () {
            var input = document.getElementById(id);
            return input.value;
        });
        var value = ctx.CurrentItem[ctx.CurrentFieldSchema.Name] || "";
        var html = String.format(editWrapper, id, value);
        return html;
    }
    var overrideContext = {};

    overrideContext.Templates = overrideContext.Templates || {};
    overrideContext.Templates.Fields = {
        'VerySimpleColumn': {
            'View': verySimpleView,
            'DisplayForm': verySimpleView,
            'EditForm': verySimpleNewAndEdit,
            'NewForm': verySimpleNewAndEdit
        }
    };

    window.SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideContext);
})();


Summary

Well, jsgrid is quite complicated. I have tried to keep it as simple and minimalistic as possible. When we can these basics, we can move on and master advanced examples where we can deliver high value to business that loves Quick Edit :)

Sources

Interesting, Anton, Andrey and me are born in Sovyet and are interested in JSGrid. Coincidence? :)

Discovering SharePoint

And having fun doing it

Bram de Jager's SharePoint blog

My view and thoughts on SharePoint.

My programming life

and everything in between

SharePoint Development Lab by @avishnyakov

It is a good place to share some SharePoint stories and development practices.

SharePoint Dragons

Nikander & Margriet on SharePoint

The Zuul Cat Idea Brewery

Where ideas on software development and entrepreneurship brew.

Paul J. Swider

Inspire! Teach! Awe!

Mai Omar Desouki - Avid SharePointer

Egyptian & Vodafoner - Senior SharePoint Consultant

Alexander Ahrens

MCPD | SharePoint | Web Development | JavaScript | .NET

Cameron Dwyer | SharePoint, Outlook, OnePlaceMail

OnePlaceMail, SharePoint, Outlook & Life's Other Little Wonders

.:paul.tavares:.

Me and My doings!

Share SharePoint Points

By Mohit Vashishtha

Jimmy Janlén "Den Scrummande Konsulten"

Erfarenheter, synpunkter och raljerande om Scrum från Jimmy Janlén

SPJoel

SharePoint for everyone

SharePointRyan.com

Ryan Dennis is a SharePoint Solutions Architect with a passion for SharePoint and PowerShell

SharePoint 2020

The Vision for a Future of Clarity

Aharoni in Unicode, ya mama

Treacle tarts for great justice

Follow

Get every new post delivered to your Inbox.

Join 249 other followers