Visual Studio LightSwitch is a great development environment for writing line of business applications that provide a consistent mobile and desktop friendly user interface that talk to various data sources including ODATA, SharePoint and SQL. SharePoint has evolved as a great platform for sharing documents and files, but LightSwitch doesn’t provide any built-in way to interact with SharePoint documents. In this article, I will give an overview of how to create a LightSwitch application that can talk to SharePoint to store the documents and make use of collaboration features of SharePoint. I will be building a simple Cookbook application that stores the recipes. Each recipe can contain files (images, documents etc.) associated with it which will be stored in SharePoint document library.
This article assumes a basic level of familiarity with LightSwitch and JavaScript. If you are new to LightSwitch, I suggest reading some of the kick starter blogs. This article assumes that you are using Visual Studio 2013 preview or later version.
Creating the Cookbook App
- First step to creating Cookbook app is to create the data and the screens needed for the Application. Follow the steps here:
- Create a new LightSwitch HTML application, name it Cookbook.
- Create a new table called Recipe and add the following fields:
- Name of type String
- Ingredients of type String
- Instructions of Type String
Now, let’s create the default screens for the table. The following steps will guide you to create Browse/AddEdit/View Recipe screens.
- Create a browse screen for the Recipe table by right clicking on the Screens node in solution explorer, and click Add Screen.
- Select Recipe as the data and click OK.
- This will create a Browse Screen for the Recipe.
- In the content tree of the BrowseRecipe screen, create a new command bar button to “Add Edit Recipe” screen.
- This will popup a Add new screen dialog for “AddEditRecipes” screen. Click OK to create the screen.
- This will create a new screen named AddEditRecipe.
- Open the BrowseRecipes screen again.
- Select the List control, and in the properties tab, click Tap action.
- Select ViewRecipes, and click OK. This will open a create new screen dialog. Click OK.
- Here is a sample screenshot of how the add/edit screen wiring is done.
Now that we have completed the basic screens, let’s go ahead and launch the application by pressing F5. You will be greeted with Browse Recipe Screen. This is the basic Cookbook app that can store recipes. Close the app, and come back to designer.
Adding Documents to Recipe
Now that we have created the recipe app, let’s add attachments (file attachments) to recipe. By adding attachments, users will be able to see the related images and other documents for each recipe. In this article, we will be showing the attachments in the View Recipe screen as a separate Tab. The documents will be stored in a SharePoint document library.
- First let’s enable SharePoint. The following steps will guide you to enable SharePoint.
- Open the Project Properties window by double clicking on the Properties node in the root project in solution Explorer.
- Click on the SharePoint tab, and you will see Enable SharePoint Button.
- Click the Enable SharePoint button, provide the SharePoint site URL and then click validate.
- When prompted for credentials, enter SharePoint credentials for the site.
- Once validated, click OK. If you have successfully enabled SharePoint, you will see a new project added your solution which is named Cookbook.SharePoint.
If you are not familiar with building LightSwitch apps for SharePoint, refer to this blog for more details.
Creating Document Library
We will be storing the files associated with the recipe in a document library. SharePoint supports two types of Storage, one is App Web which is specific to the App, and the Host Web which is shared by all Apps. In this demo, we will be using the App Web. SharePoint project provides an easy way to create Document Library. To create, right click on the SharePoint project in solution explorer, select Add New Item, select List, name it RecipeDocuments.
Click Add, and In the next screen, choose Document Library and click Finish.
This will add a new List of type Document Library to the SharePoint project. This list will be created when the App is deployed.
Adding documentViewer.css
Add a new style sheet named documentViewer.css under Content directory (by right clicking on Contents directory in HTMLClient project and click Add New Item, select StyleSheet) . Copy paste the following into the file which provides the styles for the document viewer control.
.sp-docview .sp-docview-manage,.sp-docview .sp-docview-init,.sp-docview .sp-docview-error,.sp-docview .sp-docview-list,.sp-docview .sp-docview-refresh {display: none; }.sp-docview.sp-docview-state-loading .sp-docview-spinner {display: block;background: url(Images/msls-loader-light.gif) no-repeat transparent;background-size: 18px 18px;background-position: center;height: 18px;width: 18px;margin: 2px; }.sp-docview.sp-docview-state-error .sp-docview-error {display: block;color: red; }.sp-docview.sp-docview-state-no-folder .sp-docview-init {display: inline-block; }.sp-docview.sp-docview-state-valid .sp-docview-manage {display: inline-block;text-align: right; }.sp-docview.sp-docview-state-valid .sp-docview-list {display: block; }.sp-docview.sp-docview-state-valid .sp-docview-refresh {display: inline-block;margin-left: 30px; }.sp-docview-list .ui-li.ui-btn {margin-bottom: 10px;border-bottom-width: 1px; }.sp-docview-list li .sp-docview-no-items {font-size: 14px;opacity: 0.7; }.sp-docview-list li .sp-docview-li-icon {float: left;padding-right: 8px;padding-top: 6px; }.sp-docview-list li .sp-docview-second-row {font-weight: normal;font-size: 12px;opacity: 0.7;padding: 4px 0px; }.sp-docview-list li .sp-docview-name {overflow: hidden;text-overflow: ellipsis; }.sp-docview-list li .sp-docview-author {padding-left: 4px; }
Adding JavaScript Code
Add a new script file named documentViewer.js under Scripts directory (by right clicking on Scripts directory in HtmlClient project and click Add New Item, select JavaScript) and paste the following contents which contains the logic for Document viewer control:
////// (function () {function getUrlParameter(parameterName) {var pattern = "[\\?&]" + parameterName + "=([^]*)", regularExpression = new RegExp(pattern), results = regularExpression.exec(window.location.href);return results ? results[1] : null; }var initPromise,// Icon mapping. Static mapping provides some of most commonly used icons.gifIconExtensions = { doc: true, ppt: true, xls: true, eml: true, dot: true, txt: true, htm: true, jpg: true, png: true, gif: true, zip: true, xps: true}, pngIconExtensions = { docx: true, pptx: true, xlsx: true, one: true, dotx: true, pdf: true};var sp_helper = { hostUrl: decodeURIComponent(getUrlParameter("SPHostUrl")), appWebUrl: decodeURIComponent(getUrlParameter("SPAppWebUrl")), load: function () {// Loads zero or more SharePoint objects, like a SP.User or SP.File instance. // Returns jQuery promise object that is resolved when the objects // have been loaded.var me = this, context = me.context, deferred = $.Deferred(), args = Array.prototype.slice.call(arguments, 0); args.forEach(function (o) { context.load(o); }); context.executeQueryAsync(function success() { deferred.resolve(); },function failure(unused, e) { deferred.reject(e); } );return deferred.promise(); }, getIconUrl: function (extension, large) {// Gets the URL for the icon associated with the file extension.var prefix = this.serverUrl + "/_layouts/15/images/" + (large ? "lg_ic" : "ic");if (extension === "html") { extension = "htm"; }if (gifIconExtensions[extension]) {return prefix + extension + ".gif"; } else if (pngIconExtensions[extension]) {return prefix + extension + ".png"; } else {return prefix + "gen.gif"; } }, ready: function () {function getScript(url) {return $.ajax({ url: url, cache: true, dataType: "script" }); }if (!initPromise) {var scriptBase = sp_helper.appWebUrl + "/_layouts/15/";// Check if the SharePoint libraries are already loaded. //If someone has already loaded them, skip loading.if (!window.SP || !SP.ProxyWebRequestExecutorFactory || !SP.ClientObject || !SP.ClientContext) { initPromise = getScript(scriptBase + "SP.RequestExecutor.js") .then(function () {return getScript(scriptBase + "SP.Runtime.js"); }).then(function () {return getScript(scriptBase + "SP.js"); }); } else { initPromise = $.Deferred().resolve(); } initPromise = initPromise.then(function () { sp_helper.serverUrl = $.mobile.path.parseUrl(sp_helper.hostUrl).domain; sp_helper.appWebBaseUrl = $.mobile.path.parseUrl(sp_helper.appWebUrl) .domain; sp_helper.context = new SP.ClientContext(sp_helper.appWebUrl); sp_helper.factory = new SP.ProxyWebRequestExecutorFactory( sp_helper.appWebUrl); sp_helper.context.set_webRequestExecutorFactory(sp_helper.factory); sp_helper.hostWeb = new SP.AppContextSite( sp_helper.context, sp_helper.hostUrl).get_web(); sp_helper.appWeb = new SP.AppContextSite( sp_helper.context, sp_helper.appWebUrl).get_web(); }); }return initPromise; } };// Represents the state of the control. At any given time, // the control will be one of the following states.var ControlState = {// Document library exists, but the folder hasn't been created yet.noFolder: "sp-docview-state-no-folder",// A server request is in progress.loading: "sp-docview-state-loading",// The _state is completely normal. Document library, folder both exists, // and the document list is being displayed.valid: "sp-docview-state-valid",// Error connecting to sharepoint or the document library.error: "sp-docview-state-error",// Either the document library is not configured correctly or // entity doesn't have single primary key.invalid: "sp-docview-invalid"};function DocumentViewer(element, contentItem, documentLibraryName, isAppWeb) {var _initButtonElement, _manageButtonElement, _errorElement, _listContainerElement, _refreshButtonElement, _state, _spSite, _baseUrl, _spDocLib, _docInfoList, _folderName, _spFolder, _uiState;// Gets the folder name from the entity name. Looks up for the key property // and returns the value.function _getFolderName(entity) {if (!entity || !entity.details || entity.details.entityState === msls.EntityState.added) {return null; }var model = entity.details.getModel(), properties = model.properties, keyProperties = [], value; properties.forEach(function (p) {if (p.__isKeyProperty) { keyProperties.push(p); } });if (keyProperties.length !== 1) {// More than one primary key or no primary key is not supported.return null; } value = entity && entity[keyProperties[0].name];return typeof value !== "undefined" && value !== null ? value.toString() : null; }function _setState(s) { _state = s; _refreshUI(); }function _handleError(e) { _errorElement.text(e.get_message()); _setState(ControlState.error); }function _createFolder() {// Create a folder under the document library if it doesn't already exists.var rootFolder = _spDocLib.get_rootFolder(), folder = rootFolder.get_folders().add(_folderName); _setState(ControlState.loading); sp_helper.load(folder) .then(function () { _spFolder = folder; _setState(ControlState.valid); }).fail(function (e) { _handleError(e); }); }function _refresh() {// Initialize sharepoint and extract the parameters from the contentItem value._docInfoList = []; sp_helper.ready().then(function () {if (documentLibraryName) { _spSite = isAppWeb ? sp_helper.appWeb : sp_helper.hostWeb; _baseUrl = isAppWeb ? sp_helper.appWebBaseUrl : sp_helper.serverUrl; _spDocLib = _spSite.get_lists().getByTitle(documentLibraryName); _folderName = _getFolderName(contentItem.value); _setState(ControlState.loading); } else { _spDocLib = _folderName = null; _setState(ControlState.invalid); } _refreshInternal(); }); }function _refreshInternal() {// Refresh the data by talking to sharepoint.var rootFolder, folder, files, folderNameCached = _folderName;if (!_spDocLib || !_folderName) {return; } rootFolder = _spDocLib.get_rootFolder(); _setState(ControlState.loading); _docInfoList = []; sp_helper.load(_spDocLib, rootFolder).then(function () { }).then(function () {if (folderNameCached !== _folderName) {// If the control was reset in between network request, // ignore the current result.return; } folder = rootFolder.get_folders().getByUrl(_folderName); files = folder.get_files();// Load the file collection explicitly to include specific fields.sp_helper.context.load(files,"Include(Name, Title, Author, TimeLastModified, ServerRelativeUrl, ListItemAllFields)");return sp_helper.load(folder); }).then(function () {var file, fileName, index, docInfo, extension, enumerator = files.getEnumerator();if (folderNameCached !== _folderName) {return; }// Go through the file list, and load the details.while (enumerator.moveNext()) { file = enumerator.get_current(); fileName = file.get_name(); index = fileName.lastIndexOf("."); extension = index ? fileName.substr(index + 1) : ""; docInfo = { name: fileName, title: file.get_title(), author: file.get_author().get_title(), timeLastModified: file.get_timeLastModified(), iconUrl: extension ? sp_helper.getIconUrl(extension, true) : "", viewUrl: _baseUrl + file.get_serverRelativeUrl() + "?Web=1"}; _docInfoList.push(docInfo); } _spFolder = folder; _setState(ControlState.valid); }).fail(function (e) {if (folderNameCached !== _folderName) {return; }if (!_spFolder) {// Folder doesn't exist. Set the control state to noFolder._setState(ControlState.noFolder); } else { _handleError(e); } }); }function _refreshUI() {// Refreshes the UI based on control state.if (_state === ControlState.valid) { _listContainerElement.empty(); _docInfoList.forEach(function (docInfo) {// Go through each document, and add it to the list._listContainerElement.append(" " +" " +"" +"" +"); }); _manageButtonElement.attr("href", _baseUrl + _spFolder.get_serverRelativeUrl());if (!_docInfoList.length) { _listContainerElement.append("No Items"); } _listContainerElement.listview("refresh"); }if (_uiState !== _state) {if (_uiState) { $(element).removeClass(_uiState); } $(element).addClass(_state); _uiState = _state; } } _state = ControlState.invalid; // Create DOM elements.$(element).html("" +"Manage Documents" +"" + docInfo.name + "" +"" +"" + docInfo.timeLastModified.toLocaleString() +"," +"+ docInfo.author +"" +"" +"Initialize Library" +"Refresh" +"" +""); $(element).addClass("sp-docview"); _initButtonElement = $(".sp-docview-init", element); _manageButtonElement = $(".sp-docview-manage", element); _errorElement = $(".sp-docview-error", element); _listContainerElement = $(".sp-docview-list", element); _refreshButtonElement = $(".sp-docview-refresh", element); _initButtonElement.bind("click", _createFolder); _refreshButtonElement.bind("click", _refresh); contentItem.addChangeListener("value", _refresh); _refresh(); } window.createDocumentViewer = DocumentViewer; }());
Update default.htm
Open default.htm file and make the following changes:
Add the following line after msls-2.0.0.min.css include.
<link rel="stylesheet" type="text/css" href="Content/documentViewer.css" />
Add the following line before globalize.min.js (if not already included) (SharePoint requires Microsoft Ajax)
<script type="text/javascript" src="//ajax.aspnetcdn.com/ajax/4.0/1/MicrosoftAjax.js">script>
Add the following line after msls-2.0.0.min.js file.
<script type="text/javascript" src="Scripts/documentViewer.js">script>
Add Custom Control
Now it is time to add the control to the ViewRecipe screen. To do this, open the ViewRecipe screen, and do the following steps:
- Add a new Tab under the Tabs node.
- Rename the newly added Tab to Images.
- Select the new tab node, click “Add Custom Control” and enter “Recipe” as the binding path, click OK.
- Screenshot below
- Select the newly added custom control, click “Edit Render Code” link. This will take you to the ViewRecipe.lsml.js file.
- Add the following line inside the body of Recipe_render function.
createDocumentViewer(element, contentItem, "RecipeDocuments", true);
Seeing it all Together
Now we are ready to launch the application. Press F5, and you will be greeted with the following screen.
Add a recipe, and click the recipe in the list, and click Images tab.
Now, you can see the document control is created. You can click initialize library button, which will create a new folder in the document library. The control now displays the link to the document library.
Click the link and it will launch the SharePoint document library and open the folder appropriate for the entity. The folder is named based on entity key property (which is ID in this case). You can change the logic to use different name here if you want to by updating getFolderName() function in documentViewer.js. You can add documents here by clicking the new item button.
Once you finish adding the documents, go back to the screen, and click refresh button, which will now display the documents (files) you have added.
Bingo! There is your document viewer!. You can also store documents and use SharePoint collaboration tools to edit and collaborate documents. This control provides basic functionality to interact and operate with SharePoint documents, but can be extended to do much more.
I hope you find this useful, and let me know your comments in the comments section.
- Prem Ramanathan, Senior Developer, LightSwitch Team