Getting the Weather Forecast Using SharePoint Workflows

Introduction

I already wrote a post on how to use SharePoint 2013 workflows. This time, I’ll get back to this topic, with a slightly more interesting (IMO) use case: a recurring workflow that runs once a day and emails users the weather forecast for the next day!

Let’s get started! This will work on both SharePoint 2013 on premises or SharePoint Online.

Weather Forecast

I needed a web service for retrieving the weather forecast that was:

  • Free (of course!)
  • REST (not SOAP!)
  • Using JSON (not XML!)

I ended up using Yahoo’s excellent weather web service. It does all this and a lot more, and I suggest you have a look at it.

image

I had to find out the WOEID for the city I was interested in – Coimbra, Portugal, of course – which was as simple as running this YQL query:

select woeid from geo.places(1) where text="coimbra, pt"

Then, all I had to do was pass the returned value (739672) to the query passed in the query string of the REST web service:

https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20weather.forecast%20where%20woeid%20%3D%20739672&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys

The result is this:

image

Or, in a formatted way, much easier to read:

{

   "query": {

      "count": 1,

      "created": "2016-05-14T10:13:08Z",

      "lang": "en-US",

      "results": {

         "channel": {

            "units": {

               "distance": "mi",

               "pressure": "in",

               "speed": "mph",

               "temperature": "F"

            },

            "title": "Yahoo! Weather - Coimbra, Coimbra, PT",

            "link": "http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-739672/",

            "description": "Yahoo! Weather for Coimbra, Coimbra, PT",

            "language": "en-us",

            "lastBuildDate": "Sat, 14 May 2016 11:13 AM WEST",

            "ttl": "60",

            "location": {

               "city": "Coimbra",

               "country": "Portugal",

               "region": " Coimbra"

            },

            "wind": {

               "chill": "57",

               "direction": "325",

               "speed": "7"

            },

            "atmosphere": {

               "humidity": "90",

               "pressure": "1004.0",

               "rising": "0",

               "visibility": "16.0"

            },

            "astronomy": {

               "sunrise": "6:17 am",

               "sunset": "8:43 pm"

            },

            "image": {

               "title": "Yahoo! Weather",

               "width": "142",

               "height": "18",

               "link": "http://weather.yahoo.com",

               "url": "http://l.yimg.com/a/i/brand/purplelogo//uh/us/news-wea.gif"

            },

            "item": {

               "title": "Conditions for Coimbra, Coimbra, PT at 10:00 AM WEST",

               "lat": "40.194672",

               "long": "-8.40737",

               "link": "http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-739672/",

               "pubDate": "Sat, 14 May 2016 10:00 AM WEST",

               "condition": {

                  "code": "26",

                  "date": "Sat, 14 May 2016 10:00 AM WEST",

                  "temp": "57",

                  "text": "Cloudy"

               },

               "forecast": [

                  {

                     "code": "39",

                     "date": "14 May 2016",

                     "day": "Sat",

                     "high": "61",

                     "low": "54",

                     "text": "Scattered Showers"

                  },

                  {

                     "code": "28",

                     "date": "15 May 2016",

                     "day": "Sun",

                     "high": "71",

                     "low": "54",

                     "text": "Mostly Cloudy"

                  },

                  {

                     "code": "30",

                     "date": "16 May 2016",

                     "day": "Mon",

                     "high": "76",

                     "low": "52",

                     "text": "Partly Cloudy"

                  },

                  {

                     "code": "28",

                     "date": "17 May 2016",

                     "day": "Tue",

                     "high": "77",

                     "low": "53",

                     "text": "Mostly Cloudy"

                  },

                  {

                     "code": "30",

                     "date": "18 May 2016",

                     "day": "Wed",

                     "high": "78",

                     "low": "52",

                     "text": "Partly Cloudy"

                  },

                  {

                     "code": "30",

                     "date": "19 May 2016",

                     "day": "Thu",

                     "high": "77",

                     "low": "52",

                     "text": "Partly Cloudy"

                  },

                  {

                     "code": "30",

                     "date": "20 May 2016",

                     "day": "Fri",

                     "high": "71",

                     "low": "55",

                     "text": "Partly Cloudy"

                  },

                  {

                     "code": "28",

                     "date": "21 May 2016",

                     "day": "Sat",

                     "high": "72",

                     "low": "55",

                     "text": "Mostly Cloudy"

                  },

                  {

                     "code": "30",

                     "date": "22 May 2016",

                     "day": "Sun",

                     "high": "72",

                     "low": "53",

                     "text": "Partly Cloudy"

                  },

                  {

                     "code": "30",

                     "date": "23 May 2016",

                     "day": "Mon",

                     "high": "75",

                     "low": "52",

                     "text": "Partly Cloudy"

                  }

               ],

               "description": "<![CDATA[<img src=\"http://l.yimg.com/a/i/us/we/52/26.gif\"/>\n<BR />\n<b>Current Conditions:<\/b>\n<BR />Cloudy\n<BR />\n<BR />\n<b>Forecast:<\/b>\n<BR /> Sat - Scattered Showers. High: 61Low: 54\n<BR /> Sun - Mostly Cloudy. High: 71Low: 54\n<BR /> Mon - Partly Cloudy. High: 76Low: 52\n<BR /> Tue - Mostly Cloudy. High: 77Low: 53\n<BR /> Wed - Partly Cloudy. High: 78Low: 52\n<BR />\n<BR />\n<a href=\"http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-739672/\">Full Forecast at Yahoo! Weather<\/a>\n<BR />\n<BR />\n(provided by <a href=\"http://www.weather.com\" >The Weather Channel<\/a>)\n<BR />\n]]>",

               "guid": {

                  "isPermaLink": "false"

               }

            }

         }

      }

   }

}

SharePoint Workflow

Next, I wanted to use this information in a SharePoint workflow. This workflow has to run every day automatically, and this was a challenge, because it is not supported out of the box. But, first things first, let’s see how I designed it the Weather Forecast workflow.

Initialization

Open SharePoint Designer and create a new Site workflow, make sure it is using the SharePoint 2013 settings. I first added a stage called Initialization, where I created a string workflow variable named url, to which I assigned the web service URL:

image

Next, I also created a dictionary variable named requestHeaders, to which I added a string entry called Accept, with value application/json (I don’t think this is necessary, but it is always good to tell a REST web service what we’re expecting):

image

Get Weather Forecast

On a new stage named Get Weather Forecast, I add the call to the web service itself, by adding a Call HTTP Web Service action, pointing to the contents of the url variable:

image

After I do that, I access the properties of Call HTTP Web Service action and I set the request headers as coming from the requestHeaders variable I just created:

image

Make sure you set the response to go to a new dictionary variable, responseContent. The rest is really irrelevant, but if you run into problems, it may be useful to look at the responseCode (string) and responseHeaders (dictionary) variables.

Process Response

I then added a new stage named Process Response, where I added an action of type Get an Item from a Dictionary, from which I extracted value query/results/channel/item/forecast(1)/text from the responseContent variable, which contains the result of the web service invocation, into a new string variable called result:

image

This needs some explanation. The value I’m extracting from dictionary is similar to a JSONPath – the JavaScript dotted-path to traverse an object – with two differences:

  • Instead of dots (.), we use slashes (/)
  • Instead of square brackets ([]) for indexed properties, we use parenthesis

The path we are extracting is so that we can jump directly to the textual description of the forecast for the next day, according to the schema that the Yahoo web service returns. If you use another one, you will need to change this path, of course.

Send Email

The next stage I added is called Send Email, and it’s where I send the weather forecast to the interested users. In it, I add a single action of type Send an Email,

image

In the To field, we should use a group instead of explicitly named users, something like “Weather Forecast Users”. In the body of the email, I have a mix of text and the result variable, which, if you remember, contains the portion of the web service response that has the textual description of the forecast.

Sleep for a Day

The last stage I’m adding is called Sleep for a Day, and, again, only has a single activity, Pause for Duration, configured to sleep for 1 day.


Wrap Up

This is what the resulting workflow looks like:

imageIn each stage, we need to add a transition to the next one, except in Sleep for a Day, on which the transition is going to Initialization. So, I have the following stages and transitions:

Stage Transition
Initialization Get Weather Forecast
Get Weather Forecast Process Response
Process Response Send Email
Send Email Sleep for a Day
Sleep for a Day Initialization

As for variables, I have:

image

Starting the Workflow

Now, all we have to do is, after publishing the workflow, is starting it. We do that through Site contentsSite workflows, normally available at URL /_layouts/15/workflow.aspx, and clicking the Weather Forecast link.

image

This workflow, it should be noted, shouldn’t stop, because after it reaches the final stage, it jumps to the first one, don’t forget about it.

Debugging the Workflow

The debugging experience is not great. What you can do is log stuff to the workflow history, using a Log to History List action, but be warned that you cannot log large contents: for example, logging the result of the web service call won’t work. What you can do is see where the workflow stopped and see if any exception was thrown, by clicking at the Status field of the running or completed workflow and inspecting the messages produced:

image

You also have the Workflow Health page (/_layouts/15/WorkflowServiceHealth.aspx), on which you can also see some information and stop running workflows that have gone rogue:

image

It is also worth pointing out that, unless you configured it otherwise, the output history for the workflows is stored in a hidden list called Workflow History (/Lists/Workflow%20History/AllItems.aspx):

image

Conclusion

SharePoint offers some mechanisms for integrating outside data, in the form of REST web services returning JSON, into workflows. While more complex scenarios require more advanced techniques and APIs (Business Connectivity Services, for example, or custom workflow actions), it is still possible to do some interesting stuff with what is available out of the box. Variations to this example might include conditional checks, like, only sending a warning email if the weather forecast is not good, or store the results for the next days, etc. I leave that as an exercise to you, dear reader! Smile In the meantime, do let me know how it goes!

SharePoint Pitfalls: GUID Formats

SharePoint uses GUIDs extensively. The problem is, it accepts them in different formats, in different locations:

  • The ListId and ListName properties of the DataFormWebPart, feature, site column definitions, solution manifests, all expect a GUID in the {A1ADD3D1-B21F-4F93-9B86-B1FE332424D0} format (inside { });
  • In an Elements.xml file or in a Parameter inside a data source, use B8381C7D-3B8D-46D8-8E40-C96E1FF4C308;
  • In the URL, you need to replace for %2D, { for %7B and } for %7D, as in %7BA1ADD3D1%2DB21F%2D4F93%2D9B86%2DB1FE332424D0%7D;
  • In feature receivers tokens and in New-SPTrustedSecurityTokenIssuer and the likes, GUIDs have to be in lowercase ($SharePoint.Type.edd0669b-2393-4fe6-988d-17a2De06c6e4.FullName$).

SharePoint Pitfalls: Master Pages in Page Layouts

When you deploy a page layout, you may want to add your own master page as well. I find it useful to add my own custom master pages and reference them directly, in the same or a dependent feature. You might be surprised, however, that it doesn’t work exactly how you’d expect!

The issue is, page layouts will ignore silently anything in the MasterPageFile attribute if it isn’t one of the standard tokens for the system or custom master pages. ~masterurl/default.master and ~masterurl/custom.master. The solution is to have a code behind class and specify the master page in the OnPreInit method (anywhere else won’t work):

protected override void OnPreInit(EventArgs e)

{

    base.OnPreInit(e);

 

    this.MasterPageFile = "~site/_catalogs/masterpage/CustomMasterPage.master":

}

SharePoint Pitfalls Index

This page will list all of my posts dedicated to SharePoint pitfalls. It will be updated regularly.

  1. Creating a Visual Studio Project Without SharePoint Locally Installed
  2. Save Publishing Site as Template Option Missing
  3. Publishing Pages in Document Libraries Other Than Pages

SharePoint Pitfalls: Publishing Pages in Document Libraries Other Than Pages

This one is a classic: the SharePoint Publishing feature in a Publishing Site creates a document library by the name of Pages; this is where you can store your publishing pages.

A common request is to have more of these document libraries, that is because we cannot create publishing pages anywhere else. The problem is, it is unsupported, God knows why!

What you can do is rename the document library to something else. SharePoint will look for the library id in the site’s property bag, under __PagesListId, so only need to update this value accordingly:

web.AllProperties["__PagesListId"] = newDocLibId.ToString();

web.Update();

Now, there are some solutions over the Internet that claim to go around this limitation, but none of them is supported, so proceed with care!

SharePoint Pitfalls: Save Publishing Site as Template Option Missing

If you want to save a publishing site as a template, so that it can be used to create other sites, you may find it surprising that the option is missing from the site settings page:

image

I don’t know exactly why, but publishing sites hide this option, however, it’s not difficult to get around it: just navigate to /_layouts/15/savetmpl.aspx. But, wait, what if you get this?

image

Easy, easy. Open your site in SharePoint Designer, click Site Options:

image

and change the value of property SaveSiteAsTemplateEnabled from false to true:

image

And now you will be able to access savetmpl.aspx and save your site.

SharePoint Pitfalls: Creating a Visual Studio Project Without SharePoint Locally Installed

This is the first on a (huge) collection of posts on SharePoint pitfalls. Hope you enjoy, and, please, do send me your feedback!

If you do not have SharePoint locally installed, Visual Studio will not let you create a SharePoint project:

sp-not-installed

Fortunately, it is easy to go around this in two simple steps:

  1. RegistryIn the Registry, add the following key: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Shared Tools\Web Server Extensions\15.0\SharePoint

    In it, add two string values: Location=C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\15\ and SharePoint=Installed

  2. Global Assembly CacheCopy the following assemblies from a machine where SharePoint is installed (C:\Windows\Microsoft.NET\Assembly\GAC_32) into your development machine and add them to the GAC (gacutil):
    • Microsoft.SharePoint.dll
    • Microsoft.SharePoint.Client.dll
    • Microsoft.SharePoint.Client.Publishing.dll
    • Microsoft.SharePoint.Client.Runtime.dll
    • Microsoft.SharePoint.Client.ServerRuntime.dll
    • Microsoft.SharePoint.Library.dll
    • Microsoft.SharePoint.Linq.dll
    • Microsoft.SharePoint.Portal.dll
    • Microsoft.SharePoint.Publishing.dll

    Of course, you may need others as well.

Enjoy!

SharePoint Reference Document Updated

Just updated my SharePoint reference document Excel (references: here and here) with the default content placeholders.

It now contains:

  • Fields: all the built-in SharePoint field types;
  • Content types;
  • List templates;
  • Site templates;
  • SPDataSource returned fields;
  • List fields;
  • Document library fields;
  • Search content classes;
  • Content placeholders in default master pages.

If you have any corrections or if I missed something, please let me know!

SharePoint Filter Web Part

SharePoint includes a couple of useful web parts that can be used to filter other web parts. The problem is, they are only available in the Enterprise edition. It is easy, however, to build our own filters.

The key interface here is ITransformableFilterValues; it defines the contract for passing one or more values into another web part for the purpose of filtering it. Another useful interface is IDefaultFilterValue, which, guess what, defines a default value to be returned in case no one exists to be returned. Let’s see an example that retrieves values from the query string:

public class QueryStringFilter : WebPart, ITransformableFilterValues, IDefaultFilterValue

{

    public QueryStringFilter()

    {

        this.ChromeType = PartChromeType.None;

        this.Hidden = true;

        this.MultiValueHandling = MultiValueHandling.First;

        this.MultiValueSeparator = String.Empty;

    }

 

    [WebDisplayName("Default Value")]

    [WebPartStorage(Storage.Shared)]

    [WebBrowsable(true)]

    [FriendlyName("Default Value")]

    [DefaultValue("")]

    [Description("")]

    [Personalizable(PersonalizationScope.Shared)]

    public String DefaultValue { get; set; }

 

    [WebDisplayName("Allow All Value")]

    [WebPartStorage(Storage.Shared)]

    [WebBrowsable(true)]

    [FriendlyName("Allow All Value")]

    [Personalizable(PersonalizationScope.Shared)]

    [DefaultValue(false)]

    [Description("")]

    public Boolean AllowAllValue { get; set; }

 

    [WebDisplayName("Allow Empty Value")]

    [WebPartStorage(Storage.Shared)]

    [WebBrowsable(true)]

    [FriendlyName("Allow Empty Value")]

    [Personalizable(PersonalizationScope.Shared)]

    [DefaultValue(false)]

    [Description("")]

    public Boolean AllowEmptyValue { get; set; }

 

    [WebDisplayName("Allow Multiple Values")]

    [WebPartStorage(Storage.Shared)]

    [WebBrowsable(true)]

    [FriendlyName("Allow Multiple Values")]

    [Personalizable(PersonalizationScope.Shared)]

    [DefaultValue(false)]

    [Description("")]

    public Boolean AllowMultipleValues { get; set; }

 

    [WebDisplayName("Query String Parameter Name")]

    [WebPartStorage(Storage.Shared)]

    [WebBrowsable(true)]

    [FriendlyName("Query String Parameter Name")]

    [Personalizable(PersonalizationScope.Shared)]

    [DefaultValue("")]

    [Description("")]

    public String ParameterName { get; set; }

 

    [WebDisplayName("Multi Value Handling")]

    [WebPartStorage(Storage.Shared)]

    [WebBrowsable(true)]

    [FriendlyName("Multi Value Handling")]

    [Personalizable(PersonalizationScope.Shared)]

    [DefaultValue(MultiValueHandling.First)]

    [Description("")]

    public MultiValueHandling MultiValueHandling { get; set; }

 

    [WebDisplayName("Multi Value Separator")]

    [WebPartStorage(Storage.Shared)]

    [WebBrowsable(true)]

    [FriendlyName("Multi Value Separator")]

    [Personalizable(PersonalizationScope.Shared)]

    [DefaultValue("")]

    [Description("")]

    public String MultiValueSeparator { get; set; }

 

    ReadOnlyCollection<String> ITransformableFilterValues.ParameterValues

    {

        get

        {

            var list = new List<String>();

 

            if (String.IsNullOrWhiteSpace(this.ParameterName) == false)

            {

                if (this.AllowMultipleValues == false)

                {

                    list.Add(this.Context.Request.QueryString[this.ParameterName]);

                }

                else

                {

                    var index =

                        Array.IndexOf(

                            this.Context.Request.QueryString.AllKeys.Select(x => (x ?? String.Empty).ToLowerInvariant()).ToArray(),

                            this.ParameterName.ToLowerInvariant());

 

                    if (index >= 0)

                    {

                        if (this.MultiValueHandling == MultiValueHandling.First)

                        {

                            list.Add(this.Context.Request.QueryString.GetValues(index).First());

                        }

                        else if (this.MultiValueHandling == MultiValueHandling.All)

                        {

                            list.AddRange(this.Context.Request.QueryString.GetValues(index));

                        }

                        else

                        {

                            list.Add(String.Join(this.MultiValueSeparator, this.Context.Request.QueryString.GetValues(index)));

                        }

                    }

                }

 

                if (list.Count == 0)

                {

                    if (String.IsNullOrWhiteSpace(this.DefaultValue) == false)

                    {

                        list.Add(this.DefaultValue);

                    }

                    else

                    {

                        if (this.AllowAllValue == false)

                        {

                            if (this.AllowEmptyValue == true)

                            {

                                list.Add(String.Empty);

                            }

                        }

                        else

                        {

                            list.Add(null);

                        }

                    }

                }

            }

 

            return new ReadOnlyCollection<String>(list);

        }

    }

 

    [ConnectionProvider("Query String Filter", "ITransformableFilterValues", AllowsMultipleConnections = true)]

    public ITransformableFilterValues GetFilterValues()

    {

        return this;

    }

}

There are a couple of public properties:

  • ParameterName: the query string key whose value is to be returned;
  • DefaultValue: the default value to be returned, in case the query string does not contain a value for the ParameterName key;
  • AllowAllValue: whether to allow the all (null) value;
  • AllowEmptyValue: whether to allow the empty (“”) value;
  • AllowMultipleValues: whether to allow multiple values or not;
  • MultiValueHandling: what to do if multiple values are found for the ParameterName key: select the first only, return all or combine them all into one;
  • MultiValueSeparator: the separator for combining multiple values.

The web part is invisible and will only show the chrome when the page is in edit mode. After you add it to a page, you can add a connection of type filter to another web part and select the field on the other web part that you want to filter by. The actual

ITransformableFilterValues implementation is returned by the GetFilterValues method, which is marked with the ASP.NET ConnectionProvider attribute so as to make it a connection provider, and can feed several other web parts. The logic inside ParameterValues is a bit tricky because of the AllowAllValue and AllowEmptyValue properties but I think you’ll have no problems following it.

You would normally apply a filter using the browser interface, but you can also do it in markup:

<WebPartPages:SPProxyWebPartManager runat="server">

    <SPWebPartConnections>

        <WebPartPages:SPWebPartConnection ConsumerConnectionPointID="DFWP Filter Consumer ID" ConsumerID="listViewWebPart" ProviderConnectionPointID="ITransformableFilterValues" ProviderID="queryStringFilterWebPart">

            <WebPartPages:TransformableFilterValuesToParametersTransformer ConsumerFieldNames="LinkTitle" ProviderFieldNames="TaskName"/>

        </WebPartPages:SPWebPartConnection>

    </SPWebPartConnections>

</WebPartPages:SPProxyWebPartManager>

In this example, I am binding the LinkTitle field of a list view web part to the TaskName query string parameter provided by the QueryStringFilter web part. If the query string contains a TaskName parameter that matches the list view’s LinkTitle, it will show these records.

SharePoint Reference Document

I updated the reference document for SharePoint that I first mentioned in a previous post.

Right now, I have the following lists:

  • Fields: all the built-in SharePoint field types;
  • Content types;
  • List templates;
  • Site templates;
  • SPDataSource returned fields;
  • List fields;
  • Document library fields;
  • Search content classes.

You can find it in Excel format here: https://mscblogs.blob.core.windows.net/media/ricardoperes/Documents/Reference.xlsx.

If you spot anything wrong, please let me know!