Wednesday, March 22, 2017

Creating & Deploying SharePoint Workflow Custom Activities to SharePoint Online

Creating the Workflow Custom Activity

In your project select new item and add a custom SharePoint workflow activity as below:


A custom activity comprise of three components. they are the Elements.xml, the actions4 file and the xaml file which contain the flow of the activity.


Depending on your implementation the input and output parameters in your custom activity may vary. You need to edit the actions4 file accordingly. In the custom activity I'm creating there are three input parameters. Therefore I change the actions4 file as shown below. Basically you neeed to add the RuleDesigner and the parameters section to the file.

<Action Name="GetBuildingDetailsActivity" ClassName="CustomActivites.GetBuildingDetailsActivity" Category="Custom" AppliesTo="all">
  <RuleDesigner Sentence="Create a new lease record for the given %1 and records the %2, %3, %4 ">
    <FieldBind Field="buildingId" Text="Building ID" Id="1" />
    <FieldBind Field="buildingAddress" Text="Address" Id="2" />
    <FieldBind Field="buildingPhone" Text="Phone" Id="3" />
    <FieldBind Field="buildingManager" Text="Building Manager" Id="4" />
  </RuleDesigner>
  <Parameters>
    <Parameter Type="System.String, mscorlib" Direction="In" Name="buildingId" />
    <Parameter Type="System.String, mscorlib" Direction="Out" Name="buildingAddress" />
    <Parameter Type="System.String, mscorlib" Direction="Out" Name="buildingPhone" />
    <Parameter Type="System.String, mscorlib" Direction="Out" Name="buildingManager" />
  </Parameter>
</Action>

Afterwards, edit the xaml file according to the logic you need to implement. Here note that i have created several In and Out arguments of string type.



Deploying the Custom Activities to SharePoint Online

First create the WSP. In my case I published the WSP to a local folder location.


Then upload it to the SharePoint Solutions catalog which in under the "Web Designer Galleries" section

Activate the solution
















Now we can launch the SharePoint Designer and configure the created Workflow Custom Activity to a workflow.

SharePoint Designer Configuration

  • Launch SharePoint Designer
  • Open the SharePoint site
  • Under the "Workflows" select the type of workflow you need to create. In my case I'm choosing a list workflow. So I choose the list/library which I need the workflow to get deployed as well.
  • When the workflow is generated, click on the "Stage" section which you need to add the custom activity, then select the custom activity you created.


Here, I need to point out few things.

1) My Custom Activity was packaged within a feature. Therefore I have to activate the feature in order to get the custom activities listed down in SharePoint Designer as follows:


2) Initially when the Custom Activity is added you will get the argument names supplied. You need to configure them according to the columns in your list/library. The initial view is as below:

3) Also make it a point to add a ending transition by adding a go to

A final cut of the workflow will look something similar to the below image. In this workflow I have also added another custom activity which I created separately. The custom activities will run in a sequential manner during the workflow run.

In my list the Building Id is provided from the "Title" column. Hence, the argument is changed to reflect that. Also notice the, Go to which is added to end the workflow.

 Save and Publish the workflow you created to the SharePoint list/library.
Now is when the fun begins. You can go to the list/library and test run your workflow and see how it goes !!

Additionally, you can navigate to the list/library item and check on all the workflows associated with the item. You can see stage of the workflow and how it has proceeded so far, provided that you have done the logging (write to history actions) properly. One such example would be as in the below image




Tuesday, March 21, 2017

Migrate code first solution - Entity Framework

Fire up package manager console
























Type Enable-Migrations









A Migration folder will get created in your solution


Do the code changes you need to do for the entities. In my case I have added a new property as BirthDate to my Student class

Then in the Package Manager Window, type Add-Migration with a custom name which you would like to be referred to on the solution. In the solution explorer window, you would see a new file which has been created now.



Then run the update command to update the DB like below. Your changes will get reflected in the Database




Friday, February 3, 2017

DAX queries in Power BI

The acronym DAX stands for Data Analysis Expressions. DAX is a set of functions, operators, and constants which can be used to construct formulas from existing information in your data store. if you are familiar with creating functions in Excel that knowledge comes in handy to create DAX functions.

An example for DAX function format would be like:

<MEASURE_NAME> = <DAX FUNCTION>(<TABLE_NAME>[<COLUMN_NAME>])
Eg:
Total sales = SUM(financials[sales])

Lets check out some of the DAX functions

DISTINCT
Distinct Cities = DISTINCT(Portfolio[City])

SUM
Total Sales = SUM(financials[sales])

SUMX
Allows to get the sum of an expression evaluated for each row in the table.
SUMX(<table>, <expression>)

FILTER
Filtered sales = FILTER(financials, financials[ Sales] > 100000)
Here you can create a new table to filter and get rows where sales are greater than 100000. The base table is 'financials'. The new table created is 'Filtered sales'.

CALCULATE
For this example you can refer to a sample table which I created. Refer the code in Annex for table creation.

total online sales = CALCULATE(SUM('Sales Types'[Amount]),FILTER('Sales Types','Sales Types'[Mode]="ONLINE"))

online sales 2010 = CALCULATE(SUM('Sales Types'[Amount]),FILTER('Sales Types','Sales Types'[Mode]="ONLINE"), 'Sales Types'[Year]="2010")

The total online sales put into a table would look like below


SUMMARIZE

Sales Summary = SUMMARIZE('Sales Types', 'Sales Types'[Region], "Sales per region", COUNT('Sales Types'[Amount]), "online sales count", CALCULATE(COUNT('Sales Types'[Mode]), FILTER('Sales Types','Sales Types'[Mode]="ONLINE")), "total online sales", 'Sales Types'[total online sales], "total store sales", 'Sales Types'[total store sales], "store sales count", CALCULATE(COUNT('Sales Types'[Mode]), FILTER('Sales Types','Sales Types'[Mode]="STORE")))



UNION
Union Test = UNION(SELECTCOLUMNS(Employees, "Lat", Employees[Lat], "Lon", Employees[Lon], "Type", "Employee"), SELECTCOLUMNS(Portfolio, "Lat", Portfolio[LAT], "Lon", Portfolio[LONG], "Type", "Office"))

In the last example above, I have used the UNION and SELECTCOLUMN function to get both office and employee locations in a hypothetical organization.

In order to visualize this sample you can create a new table and provide the function as shown below


















Annex

Sales Figures =
DATATABLE (
    "Year", STRING,
    "Asia", DOUBLE,
    "Europe", DOUBLE, "Australia", DOUBLE,
    "America", DOUBLE,
    {
        { "2010", 100000, 200000,80000, 100000 },
        { "2011", 400000, 250000,100000, 100000 },
        { "2012", 600000, 350000,150000, 100000 }
    }
)

Sales Types =
DATATABLE (
    "Year"STRING,
    "Region"STRING,
    "Amount"DOUBLE,
    "Mode"STRING,
    {
        { "2010", "Asia", 80000, "STORE"}, { "2010", "Asia", 25000, "ONLINE"}, { "2010", "Europe", 180000, "STORE"}, { "2010", "Europe", 125000, "ONLINE"}, { "2010", "Australia", 180000, "STORE"}, { "2010", "Australia", 125000, "ONLINE"},
        { "2011", "Asia", 80400, "STORE"}, { "2011", "Asia", 35000, "ONLINE"}, { "2011", "Europe", 185000, "STORE"}, { "2011", "Europe", 145000, "ONLINE"}, { "2011", "Australia", 175000, "STORE"}, { "2011", "Australia", 227500, "ONLINE"},
{ "2012", "Asia", 90000, "STORE"}, { "2012", "Asia", 45000, "ONLINE"}, { "2012", "Europe", 190500, "STORE"}, { "2012", "Europe", 115000, "ONLINE"}, { "2012", "Australia", 180000, "STORE"}, { "2012", "Australia", 145000, "ONLINE"},
{ "2013", "Asia", 94500, "STORE"}, { "2013", "Asia", 15000, "ONLINE"}, { "2013", "Europe", 194000, "STORE"}, { "2013", "Europe", 95000, "ONLINE"}, { "2013", "Australia", 200000, "STORE"}, { "2013", "Australia", 195000, "ONLINE"},
{ "2014", "Asia", 110000, "STORE"}, { "2014", "Asia", 225000, "ONLINE"}, { "2014", "Europe", 185000, "STORE"}, { "2014", "Europe", 120000, "ONLINE"}, { "2014", "Australia", 205000, "STORE"}, { "2014", "Australia", 225000, "ONLINE"}
    }
)

Thursday, February 2, 2017

Understanding Measures in Power BI

Measures are numbers created to create graphs. Power BI calculates and returns values according to the types of aggregation we choose for the measure. We can also create our own measures to perform complex or specific calculations.

You can create your own measures with the Data Analysis Expressions (DAX) formula language. DAX formulas are similar to Excel formulas.

In the below sample I created a new measure as 'Gross Profit'

Click on the table which you intend to create a measure, and select 'New measure'


Define the formula on the newly created measure. Note that you get IntelliSense to assist you with the formula creation.


Now you can create a graph for the 'gross Profit' by location using the created measure. Drag and drop the two related columns to the reports page


Wednesday, February 1, 2017

An Introduction to Power BI

Power BI software has a Desktop application, a web application and a mobile version. First il give an intro on the Desktop version, then will publish whats created using the desktop to the web app.

For data import you have many options. Excel and SQL data sources are mainly used by businesses.

If you choose SQL Server, then you will have to provide server details and authentication parameters. Also there are two data connectivity modes. They are Direct & Import. If you choose import the copy of the data source will be stored at Power BI, else it will directly query from db as the name implies.

The panes
In the Reports view there are two panes, namely Visualization & Fields pane. The fields pane shows the table and colum details associated with the Power BI project. So when you import from a data source the table information will be visible there. You can use the visualization pane to showcase the information in your table in chart formats and etc..


Views
There are three main views. They are Report, Data & Relationships. You can use each view update the data set. The relationship view allows you to add relationships between the data tables.



Once you are done doing the visualizations hit Publish button in the Home tabs menu. Then the project gets published to the web.


In the web site you can generate quick insights for the project. Click on the three dots which appear on your data set (in this example it's 'Sales by country'). then Power BI starts generating the insights. The result will be something similar to below.


Monday, January 23, 2017

Angular JS - Control form submit when input controls have validation errors

Did you happen to come across this scenario while using AngularJS, where you cannot stop the form being submitted even when the input controls have validation errors? Follow the below updates.

Remove the save data method from the button control. And control to the form using ng-submit

Old code

<input type="button" value="Save" name="Save" ng-click="vm.mySaveFunction()" />

Updated form code

<form name="mySampleForm" ng-submit="mySampleForm.$valid && vm.mySaveFunction()">

<input type="submit" value="Save" name="Save" />

Thursday, January 19, 2017

AngularJS ng-repeat creates a hashKey for the items in the loop

Scenario:
You load data from an Xml file and display it on your web page. You use a angular repeater (ng-repeat) to loop through the data blocks in the Xml file. After the user changes the input values you submit the data back for saving (may be back to a SharePoint document library to update the residing Xml file).

Result:
When trying to save the file back, you get an error as "Input string was not in a correct format". The Xml file in the SP library does not get updated.

Cause: 
When you inspect the updated Xml you may see that there is a hash key generated for each item with in the Xml blocks where we used the repeater.

 Example:

<Block>
   <Details>
     <Record></Record>
     <Description></Description>
     <Duedate></Duedate>
     <Cost></Cost>
   </Details>

   <$$hashKey>object:94</$$hashKey>
</Block>

<Block>
   <Details>
     <Record></Record>
     <Description></Description>
     <Duedate></Duedate>
     <Cost></Cost>
   </Details>

   <$$hashKey>object:95</$$hashKey>
</Block>

In the page your repeater attributes might look like below:

<div ng-repeat="Block in vm.data.Block">

The reason why the hash keys are generated is, because AngularJS needs to keep track of the items with in the repeater, so that it can update the DOM when necessary.

Solution:
Use a child node inside the block to keep track of the items in the repeater. You can use "track by" for this purpose. Since this provides a tracking mechanism, AngularJS will not generate the hash keys anymore.

Example 1:
<div ng-repeat="blockItem in vm.data.Block track by blockItem.Record">

Note that here I have used Record field which is unique. If you don't have any such child nodes having unique values, use the below code

Example 2:
<div ng-repeat="blockItem in vm.data.Block track by $index">


Monday, January 9, 2017

Property Promotion and Demotion in SharePoint

Once I experienced a peculiar  behaviour in the search functionality in one of the SharePoint environments. The task was to simply move a bunch of office documents in a SharePoint 2010 farm to a SharePoint 2013 farm. Yes, it was a document migration. The document metadata was scheduled to be updated in the new document library based on data in a CSV file. Had to do some digging and troubleshoot the issue.

The documents downloaded from SP 2010 web site had the metadata embedded within the document properties. The SP 2013 site had the "parserenabled" property set to true.

This has caused the metadata embedded within the documents to be Promoted to the site. That resulted in causing inconsistencies in the Search results.

So whats Property promotion and demotion? Property promotion refers to the process of extracting values from properties of a document and writing those values to corresponding columns on the list or document library where the document is stored. Property demotion is the same process in reverse.

So in my case, what happened was, the documents properties had been automatically promoted to SharePoint. When the document was added to the document library, the Document Parser extracts document property values and writes to a Property Bag (to an instance of the IParserPropertyBag Interface). And then SharePoint reads the properties from the Property Bag and promotes the applicable property values to the library based on Document Content Type or the Library Schema.








When the "parserenabled"  property is set to true in a site, SharePoint automatically captures the properties set within the office document and Promotes them.

Properties are only promoted or demoted if they match the list's columns that apply to the document.

If you need to disable document parser and stop this behaviour you could do that with a simple PowerShell script

$site = new-object microsoft.sharepoint.spsite("http://mySharePointSite.com/SubSite")
$web = $site.openweb()
$web.parserenabled = $true
$web.update()

If you don't need the Promoted properties to be available in the Search results, you could do that as well. Go to Search Service Application via the SharePoint Central Administration. Go to "Search Schema". Select the Managed Property where the document's Parsed property is set as a Mapped Crawled property. Remove the mapping with the Parsed Property.

Sunday, January 8, 2017

CRUD operations with the SharePoint lists using REST and AngularJS

In the below examples I have used the 'ABC Mappings' as my SharePoint list. The CRUD operations are done against the items in this list.

Create

var payload = '';
var itemCreated = '';

        payload = { SourceField: item.SourceField, SheetName: item.SheetName, CellCoordinate: item.CellCoordinate, DataType: item.DataType, __metadata: { type: 'SP.Data.ABCMappingsListItem' } };

        var siteUrl = window.location.protocol + "//" + window.location.host + _spPageContextInfo.webServerRelativeUrl;

        $http({
            method: 'POST',
            url: siteUrl + "/_api/web/lists/getByTitle('ABC Mappings')/items",
            data: payload,
            async: false,
            headers: {
                "Accept": "application/json;odata=verbose",
                "Content-Type": "application/json;odata=verbose",
                "X-RequestDigest": $("#__REQUESTDIGEST").val()
            }
        }).success(function (data, status, headers, config) {

            itemCreated = data.d.ID;

        }).error(function (data, status, headers, config) {

            alert('error');

        });

Read

var siteUrl = window.location.protocol + "//" + window.location.host + _spPageContextInfo.webServerRelativeUrl;

        $http({
            method: 'GET',
            url: siteUrl + "/_api/web/lists/getByTitle('ABC Mappings')/items?$select=ID,SourceField,SheetName,CellCoordinate,DataType",
            headers: { "Accept": "application/json;odata=verbose" }
        }).success(function (data, status, headers, config) {
            $scope.items = data.d.results;
        }).error(function (data, status, headers, config) {
            alert('error');
        });

Update

Initially you need to find the list item type. For this we can use the below REST call

http://<weburl>/_api/lists/getbytitle('Document Library Name')?$select=ListItemEntityTypeFullName

The function call:

        //Variables
        var payload = '';
        var itemUpdated = '';
        var itemId = item.ID;

        //Create item
        payload = { SourceField: item.SourceField, SheetName: item.SheetName, CellCoordinate: item.CellCoordinate, DataType: item.DataType, __metadata: { type: 'SP.Data.ABCMappingsListItem' } };

        var siteUrl = window.location.protocol + "//" + window.location.host + _spPageContextInfo.webServerRelativeUrl;

        $http({
            method: 'POST',
            url: siteUrl + "/_api/web/lists/getByTitle('ABC Mappings')/items(" + itemId + ")",
            data: payload,
            async: false,
            headers: {
                "Accept": "application/json;odata=verbose",
                "Content-Type": "application/json;odata=verbose",
                "IF-MATCH": "*",
                "X-HTTP-Method": "MERGE",
                "X-RequestDigest": $("#__REQUESTDIGEST").val()
            }
        }).success(function (data, status, headers, config) {


        }).error(function (data, status, headers, config) {

            alert('error');

        });


Delete

 var siteUrl = window.location.protocol + "//" + window.location.host + _spPageContextInfo.webServerRelativeUrl;

        $http({
            method: 'POST',
            url: siteUrl + "/_api/web/lists/getByTitle('ABC Mappings')/items(" + itemId + ")",
            async: false,
            headers: {
                "Accept": "application/json;odata=verbose",
                "Content-Type": "application/json;odata=verbose",
                "IF-MATCH": "*",
                "X-HTTP-Method": "DELETE",
                "X-RequestDigest": $("#__REQUESTDIGEST").val()
            }
        }).success(function (data, status, headers, config) {


        }).error(function (data, status, headers, config) {

            alert('error deleting');

        });

Saturday, January 7, 2017

Replace a specific word in a column in all rows

In this sample I am searching for all rows beginning with the name 'RED' for the DepartmentName column. Then replace the value 'RED' with 'BLUE'. For an instance such a change might come in handy in a scenario when the company is re-branding :)

(i)

UPDATE tbl_Department
SET DepartmentName = REPLACE(DepartmentName, 'RED', 'BLUE')
WHERE DepartmentName LIKE ('RED%')


If the column containing the content (In this example: DepartmentName) is of 'Text' you might get an error as:
"Argument data type text is invalid for argument 1 of replace function"

In such scenarios the column has to be casted. So the changes in the update would look like:

(ii)

UPDATE tbl_Department
SET DepartmentName = REPLACE(CAST(DepartmentName AS NVARCHAR(MAX)), 'RED', 'BLUE')
WHERE DepartmentName LIKE ('RED%')


Friday, January 6, 2017

Common Table Expression - T SQL

Common table Expression is used to specify a temporarily named results set. Below is an example.


WITH CTE AS
(
SELECT folder_id, folder_parent, folder_foldermask, folder_name
FROM tbl_dmsdocument INNER JOIN tbl_dmsfolder ON tbl_dmsdocument.document_folder = tbl_dmsfolder.folder_id
WHERE document_folder = @DocumentFolder AND document_id = @DocumentID

UNION ALL

SELECT ori.folder_id, ori.folder_parent, ori.folder_foldermask, ori.folder_name
FROM CTE AS newtbl INNER JOIN tbl_dmsfolder AS ori ON newtbl.folder_parent = ori.folder_id
)
SELECT folder_name FROM CTE ORDER BY folder_id


Wanna go crazy with the CTE and join it with another table? ... go ahead !!


Thursday, January 5, 2017

"sorry this site hasn't been shared with you" Error on site home page

If the site was working perfectly earlier and suddenly you get the error : "sorry this site hasn't been shared with you" on your site's home page.

But you notice that you still can access the site content via "_layouts/15/viewlsts.aspx".

You might also notice, that you could browse certain SP libraries but not "Site Pages" or "Site Assests" libraries.

If this is the case:

Go to the Central Admin and restart the "Microsoft SharePoint Foundation Web Application" service.

Go back to work. :D




Wednesday, January 4, 2017

Restful WCF service

Even though its a pretty old topic, thought this post would be helpful for newbie's.
Here, I created a WCF Service Application (Name: WcfService1)

Deleted the svc created by default and created my own WCF Service (Name: MyRestService)

In the interface IMyRestService add the below code

[ServiceContract]
    public interface IMyRestService
    {
        [OperationContract]
        void DoWork();

        [OperationContract]
        [WebInvoke(Method = "GET",
              ResponseFormat = WebMessageFormat.Json,
              BodyStyle = WebMessageBodyStyle.Bare,
              UriTemplate = "GetProductList/")]
        List<Product> GetProductList();

        [OperationContract]
        [WebInvoke(Method = "GET",
              ResponseFormat = WebMessageFormat.Json,
              BodyStyle = WebMessageBodyStyle.Bare,
             UriTemplate = "GetName/")]
        string GetName();
    }

in the .svc implemented the below:

public class MyRestService : IMyRestService
    {
        public void DoWork()
        {
        }

        public List<Product> GetProductList()
        {
            //Product is a class which I used to load product details from repository
            Product p = new Product();
            return p.GetProducts();
        }

        public string GetName()
        {
            return "Hello World";
        }
    }

Solution structure is



In the web.config do the below settings

  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior name="serviceBehavior">
          <serviceMetadata httpGetEnabled="false"/>
          <serviceDebug includeExceptionDetailInFaults="false"/>
        </behavior>
      </serviceBehaviors>
      <endpointBehaviors>
        <behavior name="web">
          <webHttp/>
        </behavior>
      </endpointBehaviors>    
    </behaviors>

    <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
 
    <services>
      <service name="WcfService1.MyRestService" behaviorConfiguration="serviceBehavior">
        <endpoint address=""
                  binding="webHttpBinding"
                  contract="WcfService1.IMyRestService"
                  behaviorConfiguration="web"></endpoint>
      </service>
    </services>
  </system.serviceModel>

Now you can test the methods using the urls:

1) http://localhost:63337/MyRestService.svc/GetName/
2) http://localhost:63337/MyRestService.svc/GetProductList/



Monday, January 2, 2017

Copy Ranges of Excel Sheet to Word Document using Office Interop

In the below sample I copy a cell Range in a Excel sheet into Word document using Interop

Refer the Excel and Word Office Interop DLLs

Browse and provide the DLL locations. For example:

C:\Program Files (x86)\Microsoft Visual Studio 12.0\Visual Studio Tools for Office\PIA\Office15\Microsoft.Office.Interop.Excel.dll

C:\Program Files (x86)\Microsoft Visual Studio 12.0\Visual Studio Tools for Office\PIA\Office15\Microsoft.Office.Interop.Word.dll

Add using directives

using Word = Microsoft.Office.Interop.Word;
using Excel = Microsoft.Office.Interop.Excel;

Declare a variable in your class

object oMissing = System.Reflection.Missing.Value;

In your method create a Excel Application to copy content and a Word application to paste into

            Excel._Application xlApp = new Excel.Application();
            xlApp.DisplayAlerts = false;
            xlApp.Visible = false;
         

            Word._Application wdApp = new Word.Application();
            wdApp.DisplayAlerts = Word.WdAlertLevel.wdAlertsNone;
            wdApp.Visible = false;
            Word.Document document = null;

            try
            {
                Excel.Workbook workbook = xlApp.Workbooks.Open(filePath);
                Excel.Worksheet worksheet = workbook.Sheets[8];

                document = wdApp.Documents.Add();

                worksheet.Range["A2", "I120"].Copy();
                document.Range().PasteSpecial();
                document.SaveAs2(@"C:\temp\1\Mydoc.docx", Word.WdSaveFormat.wdFormatDocumentDefault, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing);
                workbook.Close(false, oMissing, oMissing);

            }
            catch (Exception ex)
            {
                //TODO - log exception
            }

            finally
            {
                if (xlApp != null)
                    xlApp.Quit();
                if (document != null)
                    ((Microsoft.Office.Interop.Word._Document)document).Close(oMissing, oMissing, oMissing);
                if (wdApp != null)
                    wdApp.Quit();
            }

In the above code segment if you know the name of the workbook sheet instead of using he code line:
Excel.Worksheet worksheet = workbook.Sheets[8]; you could use a variable to hold the sheetName and update the code line as below:
Excel.Worksheet worksheet = workbook.Sheets[sheetName] as Excel.Worksheet;