Sitecore Experience Forms Walk-through

With Sitecore 9.0 we have the release of the brand new Experience Forms which is quite different from good old Web Form for Marketers. I had a hands-on experience with this module and performed some investigation on its architecture. The video is a recording of the briefing session I conducted with my team members. Hope you find it useful as well.


Key takeaways

WFFM and Long history of javascript issues

If you have ever used WFFM on your page, you know that it has its own set of scripts. The way these script injected to the page has evolved which confuses a lot of front-end developer whether they are upgrading their instance or developing new pages. In this article we are going to take a look at the journey WFFM went through (scripts only) Since Sitecore 7.2 to the latest release 8.2 and also discuss the common bugs faced with each version. The focus is on MVC pages which are widely used and has more known issues.

WFFM v2.4 rev.151103 and Sitecore 7.1 – 7.2

This version assembles RequireJS. The implementation is quite buggy and troublesome. Below are is some of the known bugs of this version.

Bug ID = #80478

Description: WFFM does not work with other JavaScript libraries which use define() function. Some JS libraries that use this function are Slick.js, jquery.cookie.js, and bodyBottom.min.js. error message is something like “Error: Mismatched anonymous define() module:”

This error can also cause misbehavior in page/experience editor. After adding a MVC form, page/experience editor will stop working.

Workaround : request Sitecore for a patch.

Resolution: Fixed in WFFM 8.1 Update 2

fixes include a number of JavaScript errors that appeared in multiple internet browsers and also a number of JS files that have been reviewed to ensure consistency. (80478, 78916, 82721, 85115, 83997)

Bug ID = (unknown)

Description: RequireJS is attached to global scope and does not let you add your own scripts to the page using this library.


WFFM 8.0 rev. 141217 (Update-1)

Since this release, requiredJS has been excluded from the module.

window.$scw = jQuery.noConflict(true);  

line is added to ~\Website\Views\Form\Index.cshtml so there should be no more conflict issue with your website jQuery

WFFM 8.1 rev. 151008 (initial release)

RequireJS is now back again without much improvement from the previous implementation in WFFM v2.4

In this version again there is no sign of jQuery.noConflict in WFFM implementation. So there should be conflict issue with your site specific jQuery library.


WFFM 8.1 rev. 160523 (Update 3)

Appearance of the following setting. This setting determins if bootstrap.min.css should be added to WFFM form rendering or not.

<setting name=”WFM.EnableBootstrapCssRendering” value=”false” />

$.noConflict(); is added to main.js file so there should be no issues with having two jQuery on a page.


There have not been much changes related to Javascripts in WFFM since 8.1 Update 3 release

DevCon 8 – MS Excel Beyond Formulas and Macros

Microsoft excel is a powerful tool used by billions of people around the word, programmer as well. Power users are able to perform arithmetic operations using macros and formulas. However, excel has a hidden gem that opens a new paradise for programmers. The ability to modify excel sheets using Visual Basic for Application (VBA) directly from Excel interface. In this post, I am going to share my experience using this tool with you all,

Enabling Developer Tab

  1. Click the File tab.
  2. Click Options.
  3. Click Customize Ribbon.
  4. Under Customize the Ribbon and under Main Tabs, select the Developer check box.

Writing the First Function

  1. Navigate to Developer tab and click on Visual Basic logo
  2. Right click on Microsoft Excel Objects folder in the left tree and navigate to Insert -> Module
  3. This is the playground, let’s write some hello word code. Copy past the following code to your module.
Public Sub HelloWorld()
    Columns("E:E").ColumnWidth = 65.29
    Rows("8:8").RowHeight = 210
    With Selection
        .FormulaR1C1 = "Hello World"
        .HorizontalAlignment = xlGeneral
        .WrapText = False
        .Orientation = 0
        .AddIndent = False
        .IndentLevel = 0
        .ShrinkToFit = False
        .ReadingOrder = xlContext
        .MergeCells = False
        .VerticalAlignment = xlCenter
        .Font.Size = 70
    End With
    With Selection.Borders(xlEdgeTop)
        .LineStyle = xlContinuous
        .ColorIndex = 0
        .TintAndShade = 0
        .Weight = xlThin
    End With
    With Selection.Borders(xlEdgeBottom)
        .LineStyle = xlContinuous
        .ColorIndex = 0
        .TintAndShade = 0
        .Weight = xlThin
    End With
    Selection.Borders(xlInsideVertical).LineStyle = xlNone
    Selection.Borders(xlInsideHorizontal).LineStyle = xlNone
    ActiveWindow.DisplayGridlines = False
End Sub
  1. Now Press the run button or press F5 to execute the code

The beauty of VBA is that it does not require any explanation. It’s pretty self-explanatory and if you have programming background, you should be able to read and understand it. By coming this far, you already have what you need to start programming your excel sheet. Below is some of the techniques that I learned during my time working with excel that you might find it useful as well.

Macro is your best friend

When you record a Macro, Basically Excel will follow your every move and logs it as a VBA script. This is very useful when you are looking for a code to perform a certain action. For example, you want to create a pivot table and assign it a data source. Googling it will not help you much since there are very limited resources out there. So all you have to do is to perform the operation using Microsoft Excel UI while recording a Macro. After stopping the Macro, you can find the related code in the Excel Code editor (new module is automatically created for a new Macro).

Excel Objects

Basically, there are two types of objects in Excel

VBA Objects

VBA objects are built in the application and can be created using Dim and/or Set keyword. These objects consists of Event, Method, and property. To find out the full list of available objects press F2 in Visual Basic Editor. Here is a sample code and some of the useful objects that you can use.

 Dim wk As W&lt;code&gt;rkbook

Set wk = Workbooks.Open "C:\Docs\Accounts.xlsx"

wk.SaveAs "C:\Docs\Accounts_Archived.xlsx"

Dim collFruit As New Collection

' Add item to the collection

collFruit.Add "Apple"

Dim a As Worksheet

Set a = ActiveWorkbook.ActiveSheet

a.Cells(2, 2) = "Some text"

Note: You can use the Set keyword solely (without using dim to define the object). Despite the technical differences between two method, the biggest drawback of using Set alone is that you won’t be getting any intellisense since object type is unknown to the code editor.

You also can subscribe to an Object event by writing a procedures like the one below:

 Private Sub Worksheet_Activate()

MsgBox "Sheet1 has been activated."

End Sub 

COM objects

These object are not Excel native and derived from other applications. These include the Dictionary, Database objects, Outlook VBA objects, Word VBA objects and so on.

Set dict_Month = CreateObject("scripting.dictionary")
If Not dict_Month.exists(.Cells(i, 1).Value) Then
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; dict_Month.Add .Cells(i, 1).Value, CreateObject("scripting.dictionary")
End If

Connecting to an Access database:

 Public Function RunQuery(ByVal strDBPath As String, _
                     ByVal strQueryName As String, _
                     Optional ByVal execOption As Integer = 4, _
                     Optional ByRef rowAffected As Long = 0, _
                     Optional ByVal showError As Boolean = True _
                     ) As Object
    Dim objCmd As Object
    Dim objRec As Object
    Dim varArgs() As Variant
    Set objCmd = CreateObject("ADODB.Command")
    With objCmd
        On Error Resume Next
        .ActiveConnection = "Provider=Microsoft.ACE.OLEDB.12.0;" & _
                             "Data Source=" & strDBPath & ";" & _
                             "Jet OLEDB:Engine Type=5;" & _
                             "Jet OLEDB:Database Password=""" & DBPassword & """;" & _
                             "Persist Security Info=False;"
        .CommandText = strQueryName
        'If UBound(parArgs) = -1 Then
            Set objRec = .Execute(RecordsAffected:=rowAffected, Options:=execOption)
            If Err.Number <> 0 And showError Then
            'Display an error message to the user.
                MsgBox "Ooops. There was an error while generating report! Please contact administrator if the problem persists", vbCritical, "Generation Error"
                Sheets("start").Range("A30").Value = Err.Description
                Sheets("start").Range("A31").Value = strQueryName
            End If
    End With

    Set RunQuery = objRec

    Set objRec = Nothing
End Function

Global Object and Events

You have the option of having workbook-wide variable or set some variable values at workbook start up. All you need is a procedure with the event name in the ThisWorkbook object. For example to perform some warm up activity when the sheet opens:

Private Sub Workbook_Activate()
   Worksheets(1).Cells(1, 1).Value = "I am here:)"
End Sub 

Full list of events:

It is also possible to have Workbook wide variable using Global Key word

 Global Const aSheetName = "Detail Report" 

However, I would recommend having a hidden sheet where you set your constant values. This is useful to save memory and also have the value saved for next usage.

TFS and Excel – The programmer heaven

Another hidden treasure of Excel is its ability to connect to TFS. You are able to bulk insert your TFS items into the Server or download everything from server and use Excel tools to perform some Analysis. The full tutorial on this can be found from the link below:


You may download the Slide from here: DevCon8 – MS Excel

Financial Management Excel Sheet can be downloaded from here: ExcelFinancialManagement_v1

DevCon #5 Presentation on Reveres Engineering

This post is regarding my Tech talk on  Reverse engineering and debugging .Net applications conducted on Herbalife Malaysia Office as part of DevCon event. DevCon is a monthly gathering of Microsoft enthusiast to deliver and discuss advanced programming knowledge. I participated as a speaker for DevCon #5 and here is a brief explanation of the topic.

There are many cases, that we have to work with third-party assemblies in our solutions. It is all cool until we run into a exceptions, bottleneck or some random message in the log file. Usually, the first thing comes to our mind as a developer is a bunch of swear world for the developer of the component and finally contacting their customer support with a baggage of complaints. However, this is not always the best option as support usually take a long time to investigate the scenario and we all have deadlines (who doesn’t?). Things could be even worse. They might find that issue was with you and not the component and they might as well use some swear words.  Therefore, if you wish to save yourself from embarrassment or make your complaint baggage heavier, you have to do some detective work yourself. In this session, we will walk through the reverse engineering tools and use them to sneak peek into .Net assemblies.

Here is the link to download my slides

Reverse engineering and debugging


And here is a recording of my presentation.

Part 2

Pine Tree


Pine Tree (still not sure why is it called that!) is located at Fraser Hills, approximately 2.5 hours drive north of Kuala Lumpur. The place is known for it’s amazing weather and beautiful nature.


It’s been a while since we were aiming for this hike. During the raining season it’s mostly closed so we had to wait until February. The trip from KL was quite long. 100 KM is a long way but we couldn’t give up on this hike.

We encounter very limited number of hikers during the hike. Mostly random people hiking without proper gears, using flip-flop or “Kampung Adidas”.

The Trail

It is a Moderate-hard hike. The hike was supposed to be 7 to 8 hours according to the sign board at the start point. To me it looked more like a warning than an information board. I think many non-hikers take that path without having any prior understanding of what lays before them. However, we mange to get to the the first high ground in 2 hours and conquer the twin peak (second summit) in 2.5 hours. 1 hour rest and 2.5 hours coming back. It is worth noting that we did not take a break (excluding short water breaks) and maintain a steady pace through-out the journey (no bragging here ok?). This hike is unique in a way. It takes you almost same time to going back due to it’s unsteady up and downs.

Path was very muddy during our trip but no leeches (thanks god for that). It’s clear that it has been altered for non-professional hikers. Some rope and concrete stairs along the way. However, land slide seems to damage the work they have done. Close to the first peak, there is a part that we had to climb up an almost horizontal ground. There is a rope but have to be careful there. It is a bit tricky.

Interesting part is that after the first peak, there is a noticeable change in the vegetation. We can see different type of trees and the nature feels more virgin. That’s probably something I can look up, or if you know about it, please share with me in the comments.

The Good

The Bad

Is it good for camping?

There are no many camping spot along the way. Officially, camping is not allowed but the twin peak would be an ideal spot. There are no source of water nearby so bring enough with you.

Hike Statistics

Click on the hiker icon to see more details.

Sitecore WFFM – Add a User to a List

Web Form For Marketers is one of the many Sitecore modules. This one specifically aims non-tech people and helps them create web forms. This module is very power-full and also inherits Sitecore legacy of being”Extensible and configurable”. We can modify form appearance, input types, validations, submit actions, etc. WFFM is regularly used to add comment section to a post, create user sign up form, feedback and contact us forms or anything of that sort. One of it’s common use is to add a person to an email subscriber list. Fair enough. Good move.  However, Sitecore Developer or Marketers seems to be getting it wrong. Here is how it must be done.


Let’s Get down to business

O’right. this is very straight forward.

and we are done!?


The mistake we all make is:

We assume “Add Contact to Contact List” will get the form data and put that user in the specified list

What it actually does is:

It will add the current analytic contact form Tracker.Current.Contact property to the specified list.

This will not work because lot’s of time the user is not identified. Therefore, Analytics does not have proper contact information to send an email to. Even though this feature has been requested from Sitecore, there is no ETA on when it is going to be available. Meanwhile, there are 3 ways to make this work

  1. Only let authorized users access the form – Not ideal from a marketer perspective.
  2. Make sure the user is identified before triggering “Add Contact to Contact List” action – Requires communication with Sitecore developers and error prone
  3. Create a Custom save action that identifies the contact + add the contact to the list – What we are gonna do

You might be asking yourself why not just add the user to the list without identifying it? And the answer is: Sitecore List Managers uses Analytics contact in the variety of places and for the variety of reasons. If you find a way around it, you might never know where things might go wrong. So my advice is to leave the back door alone and stick to the front door. It’s safer that way 🙂

How to do it?

1. Create the visual Studio project and past this code to the project (or just clone the project from the repository). Build the DLL and place it in your bin folder (e.i C:\inetpub\wwwroot\hostname\Website\bin )

2. Create the patch file like the one below. You may change the value for “emialFieldName” and “firstNameFieldName” parameters according to your corresponding form’s field name.

3.  Copy the new config file to your website include folder (e.i C:\inetpub\wwwroot\hostname\Website\App_Config\Include\Wheelbarrowex)

4. Login to your Sitecore instance as admin and  navigate to /sitecore/system/Modules/Web Forms for Marketers/Settings/Actions/Save Actions

5. Right Click on “Add Contact to Contact List” action and click on Duplicate and give it a name in the pop-up message

6. Change the value for Factory Object Name to “/sitecore/wffm/actions/identifyAndAddContactToList” under Data section so the action knows where are the configuration.

7. Change the value for Display name to “Identify and Add Contact to List” under Appearance section. This will change the name in the tree so we can easily find the action later on.

Note: Appearance section is not displayed by default. Check the Standard Fields checkbox under View tab to enable it.


8. Now open form designer and assign your form a new action.

9. Select the list you wish to add contacts to.

10. Save the form

11. Smart Publish the site.

Things to consider

Gunung Liang

Gunung Liang for the first timers is NOT RECOMMENDED. If you haven’t done hiking for sometimes, better choose more easy hikes like Gunung Datuk, or Bukit Kutu. This can be both dangerous and discouraging.


During Chinese new year, Malaysian get to enjoy two days of public holiday. This is when many people get behind the wheel and start driving around the country. What we did was going for a hike that cannot be done on a short weekend. So we decided to try Gunung Liang. None of us has ever been there. Unlike other hikes, there were absolutely no other hikers/cars at the start point.

The Trail

The trail is sometimes hard to find and full of Freighthoppings. Hike begins at side of a close gate. We were not able to find the start point without a help of a man who was camping at the nearby river. He showed us some small steps at the side of a tree at the right side of the gate. We wen’t off the track at first but slowly got a hang of it. There are some red stripe plastic pieces tied around the trees along the trail. It is easy to locate them if you are on the right path. There is one river crossing that we spent sometimes locating the right way. The rule of thumb is: if the path ends up to a river, tail of it is right across.

If you are going to hike there in peace, you need to be prepared for leeches. These unforgiving bastards are everywhere :). Specially the first two Kilometer of the trail. It’s recommended to wear long pans and check for these intruders every now and then. There are other means to avoid them using salt, leech socks etc but be prepared.

What’s Good about it?

Well I would say it is adventurous. The place is kind of virgin. You get the true sense of what it is to be in a real rain forest. Variate of insects and kind of wild atmosphere.

The river is also good. Water is so fresh and there are perfect spots to dip-in if you like and I recommend it. However, look out for the leeches. They live in water as well and you better don’t step into the sandy parts of the river.

Last and most important of all is the summit. Oh it’s lovely. When we got there, there was a magical mist around the soaring trees that replicate a scene from the Lord Of the Ring. To the point we started to think how would it be to hike with some Org folks :D.

Is it good for camping?

There are couple of camping sites along the way but the best is at the summit. If you wanna camp at the side of the slim river, be prepared for irritating bees. If we factor out the bees and leeches and other creatures around there,himmmm, it is still not good for camping 🙂 . However, the summit makes a good camping location if it is not already occupied (capacity is 2 to 3 tents). The weather is really good up there and it gets cold in certain points. There are fire places as well from previous camps.

Hike Statistics

Gunung Nuang – One hike to remember

Gunung Nuang is a bit different than other hikes. If you haven’t been hiking for sometimes, you better reduce the expectations and forget about hitting the peak.

Our journey began roughly at 7:30 AM from Endah Parade, Bukit Jalil. After a quick breakfast and a chitchat, finally we were on the road. The navigation took us through all village areas and a narrow road. The road can be a little dangerous if you are not careful. However, there are no tolls.

The drive was about one and half hour from Bukit Jalil and we were at the start point by 8:45 AM. As usual there is a registration book that you have to sign prior to the hike and of course a fee of RM1. With that sorted, we were off with a great start. Lot’s of smiles and happy faces. I was a a bit of a bummer since I knew this smiles will not last for too long (Evil Face was on). This was the second time for me hiking these heights and I perfectly remember the last 20 minutes of the hike 🙂 .

After about an hour, we started to split up. We were divided into 3 groups of fast, medium and slow hikers. We didn’t plan such thing and we kind of got forced into it. This usually happens when you don’t know the ability of the people you hike with. It’s fine to do so as long as no one is left alone and each groups has minimum of two members.

This mountain is really beautiful. There are a few river crossing along the path. After about 1.5 hours, we reached to a waterfall that is perfect for a dip. We marked this location and we set it as the prize for the hike. After this point, the gap between the groups spread and we didn’t see each other.

I was with the medium speed hikers group, walking slowly and appreciating every minute of the hike. It is really recommended to slow down a little and spend more time observing the fascinating ecosystem surrounding you. This forest is housing huge number of monkeys. When you get closer to the peak,  their voice echoes and breaks the silence. In my opinion, this is the best part of hiking in tropical rain forest.

As we get closer to the 3th camp, one of my friends was down with a severe muscle cramp. It was too bad that he couldn’t walk anymore. With the help of other kind hikers, we applied some muscle relaxant on his leg. It was not effective since he also had low blood pressure and sugar level. Luckily, we were not too far from the camp site so I could carry him for a short distance. We made him warm and comfortable so he could rest while we continue to the peak.

With one down, two of us stayed hopeful to reach the summit. We knew the turn around point for Nuang is 2PM. It was about 12:30 that I got a call from the fast group, checking in. They haven’t reached the top and they were concerned that we wouldn’t make it on time. No matter what, we staid strong and kept moving.

As soon as we reached the last camp side, my other companion yielded. He also was feeling low blood pressure. Considering today’s experience, he decided to call it off and stay back and rest until we come back. Now it was me alone with a decision to make. I couldn’t bare the thought that I would go back not achieving my goals. I was deep in my thoughts that I saw two middle aged ladies walking up to the camp side. They both saw what happened to our group and found me puzzled. They throw in a word of encouragement and that was enough to tantalize me. So I joined them and continue the hike to the peak.

It was about 2PM that I finally made it to the summit where I was greeted with lots of cheering and hand shaking from my friends and other hikers. It was indeed a great achievement. I couldn’t be more proud that day to finally make it despite all the events. I sincerely thanked the two ladies for their help, had a short rest and started descending.

On the way down we were meeting one team member at every camp. We had finally the full team heading down together. And as we planed, we stopped at the waterfall to boost the joy to the max. The cold water on my heated buddy is exactly what I needed. Listening to the sound of water and staring at the nature beauty is what sets me up for a hard working week ahead. Isn’t that just amazing? From there it was all downhill until it start pouring.

Malaysia and moonsoon what can you expect? We were ready for this and we all had a rain coat. Obviously it couldn’t keep us dry but helped to save out items in our back-pack especially electronic devices. We just made it more interesting by running the rest of the path to the start point. woooh. A memory I will never forget 🙂

In short, Gunung Nuang is a rewarding hike if you are fit enough to enjoy it. Never over-estimate your abilities because it wouldn’t be easy for your companion to carry you down. Hiking is not only about climbing up and having some physical activity. It’s more about rebounding with the nature and receiving energy from our lovely planet. It’s about friendship and seeing the kindness in mankind. You don’t see people angry in the forest. All you see is smiling happy faces, having a great time.

Hike Detail

Here is all statistics you need to know about the hike.

Sitecore SPEAK – A quick start

SPEAK or Sitecore Process Enablement & Accelerator Kit is a framework to quickly and easily create a custom application in Sitecore Experience Platform. In this blog post, we are going to create an app that displays some Sitecore items in table format. Please note that the goal here is to get SPEAK App running as quick as possible and explanations are summarized to a link for further reading.


Let’s Get Rocks rolling

Launch Visual studio and head to Sitecore Explorer.



Now that we have our connection ready let’s create the APP

Creating the App

Before we create the app, we need to know what type of SPEAK App suits our needs. You can read about them from here. For this blog, we favor ListPage because it best matches our requirement.


Since we all are fans of rewarding work, let’s head to http://YourInstanceName/sitecore/client/Your%20Apps/MyFirstApp/LandingPage and view the result of our hard work. Yeah, it’s pretty much nothing because there was no hard work. If you are wondering what is that “List Page Title” on your new app, proceed to the next session.

Let’s Beef it up

Now that we have the page, it’s time to add our stuff in there. The plan is to display a set of values in table format so let’s start with creating the items holding our values.



Now that we have our templates, we need to create a set of fake data to be displayed.


Now that we have our dummy data, we got to display them. We need two more items in our SPEAK app to be able to do so.

Firstly, we need a DataSource. If you don’t know what that is please take a look at this articleSearchDataSource is very useful. Currently, we need it to to feed our items into a ListControl (or simply our table). So let’s create it.


The SearchDataSource is now created and waiting to be configured. There are two ways of configuring a data source. The right way and the wrong way.

The right way is when we create a separate SearchDataSource Parameters item and set it as the data source for our SearchDataSource. This way we can make our item extensible and easy to maintain.

The wrong way is when we change the SearchDataSource properties without any Parameter item.

To keep this post simple, we follow the wrong way. It can be used for testing purpose but never use it in production.

Done with data source. Now let’s create our ListControl which is our table


Unfortunately, there is no wrong way of configuring a ListControl. Therefore, we need to create a ListControl Parameter item for it


MyListParam will be our table column container. Now let’s add some column into it.



And finally, we need to tweak our ListControl property to read the parameters.

Yes. The moment we all were waiting for is here. Now head to http://YourInstanceName/sitecore/client/Your%20Apps/MyFirstApp/LandingPage and see the result of your effort.

One more thing,

List Page Title is still there and it’s annoying, right? let’s head back to LandingPage.layout and find the HeaderText Rendering. Change it’s Appearance->Text property to “My page, My title :p” and save the changes.



I am not gonna load you with more information here. This post is aiming to lift you up and leave exploring SPEAK capability to you. Try to create parameter item for renderings we created and Item and explore their fields to achieve more goals.

Gunung Angsi Hiking


There are times that you wish to clear your mind from negativity, get into the nature and listen to the sound of life but your friends are just too lazy to go too far for it. So if you are in that stage, Gunung Angsi is a good choice for you.

The starting point is roughly 1 and half hour drive south of KL. Once you take the exit from the main road, you are about 100 meter away from the starting point (awesome navigation right :p). There is an entry fee (RM5) but when we came back, the mountain marshal or whoever that guy was didn’t show up and we didn’t signed out. We didn’t even get a call confirming our safe return. So the 5RM is almost for no reason and you better don’t rely on them to save your life.

The hike was expected to be 3 hours up, so we were prepared. However, we did it in 1 hour and 25 minutes (as you can see from my watch report below). While I was announcing this glamorous achievement to my friends proudly on the peak, someone voiced out that the 3 hour trail is from the other side of the mountain. I didn’t honestly check that statement but it reduced my excitement anyways 🙂 . Still there is no question about our fitness and status of “Fast Hikers”

The track was somehow similar to Gunung Datuk, high slop at first and more chilled later. One of the muddies I ever seen. It could be due to the moonsoon season in Malaysia. There was no river crossing in our path and couple of look out points. The peak had a nice view as well but not that special compare to other peaks.

Bottom line, it is a nice and easy hike that can brighten your day with a good morning activity. Do check it out. Do not forget to comment.

Hike Detail

The Course