-
Notifications
You must be signed in to change notification settings - Fork 0
Daily Updates
Decision made : To create a survey poll app in Sharepoint and show the real time updates to polls in one app part
- Created solution
- Added project SharePointPollApp (TODO: Add screenshots)
- Create new project by selecting App for Sharepoint
- Put in your sharepoint tenant Url
- Select provider hosted app
- Select ASP .NET MVC app
- Created a new List in the same site collection as the app is installed in (TODO: Add screenshots)
- Add an app
- Select Custom List
- Name : SurveyList
- Once created go in settings => create new column
- Create a column 'Options' with Multiple Lines text type
- Add an entry in the list (Title => Question, Option => one option on each line)
- In the MVC app web, renamed the Home controller as Survey Controller
- Created a Survey Model
public string SurveyId { get; set; }
public string Question { get; set; }
public List<string> Answers { get; set; }
public string SelectedAnswer { get; set; }
- Creating the functionality to show the survey questions to user:
-
After a lot of googling found a way to access the list in the host web
` List surveyList = clientContext.Web.Lists.GetByTitle("SurveyList"); clientContext.Load<List>(surveyList); clientContext.ExecuteQuery(); CamlQuery query = CamlQuery.CreateAllItemsQuery(); ListItemCollection items = surveyList.GetItems(query); clientContext.Load<ListItemCollection>(items); clientContext.ExecuteQuery(); foreach (var item in items) { model.Question = item["Title"].ToString(); }
-
`
This code can be found in many blogs but the important thing to remember here is: **When accessing a list from Host Web (A list created through an app is in the app web) we need to give the app a permission to "Read" in appmanifest. (put exactly the above lines in app manifest irrespective of any tenanat)**
```
<AppPermissionRequests>
<AppPermissionRequest Scope="http://sharepoint/content/sitecollection/web/list" Right="Read"/>
</AppPermissionRequests>
```
- Created the view to display question and give options for answers
While coding the view and creating the model for it I realized that having a Multiline text field or comma separated list in a single line text field for "Answers" won't do as I need to store the vote count for each answer. I was actually trying to follow the design of already created SmartPoll app. But this single list was not useful to me. So I changed my Survey list to store Question Number and Question and created one more list which stores Survey Answers. (Question Number, Answer, VoteCount) following the same steps as above.
After my lists were created and populated with some test data. I added code to get the Survey model. Here is where I realized now I need my app to have read access on two lists. With above mentioned entry in appmanifest, it asks while installing the app which list you want the app to have read access on and shows a drop down list of all the lists in the site collection. This is not useful for two reasons:
- We need the app to read more than one list.
- User who is installing the app does not know the lists app uses.
After googling for a while I found a solution:
You have to give the app read/write permission at web scope and not at the list scope. I have given Write permission as we'll need the app to update the vote count column of the list later.
So you have to add below line in the app manifest:
<AppPermissionRequests AllowAppOnlyPolicy="true">
<AppPermissionRequest Scope="http://sharepoint/content/sitecollection/web" Right="Write" />
</AppPermissionRequests>
-
Completed the code to fetch the lists and populate the model.
-
Created the Partial View to display one question at a time
-
Minor javascripting to show next question
-
Functionality to submit the user's vote
Wrote an Ajax post on submit to a "SubmitVote" action method.
[HttpPost]
public ActionResult SubmitVote(SurveyModel surveyAnswer)
I had written code to access the list in the action method so I copied the code from Index action to create the SP context and client context. But the SP Context was being returned as null and thus the CreateUserClientContextForSPHost method was failing. A few blogs suggested that I need to put ShrePointContextFilter attribute to my Post Action Method. Two important points that I found (Check the full blog):
- The SharePoint Context Filter attribute performs additional processing to get the standard information when redirected from SharePoint to your remote web application, such as Host Web Url. It also determines whether the app needs to be redirected to SharePoint for the user to sign in (in case of bookmarks).
- You can apply this filter either to the controller or to a view. SharePoint Context classes encapsulate all the information from SharePoint so that you can easily create specific contexts for the app web and host web right away and communicate with SharePoint.
BUT, it led to an another error. I got the below response for my post request.
** An unexpected error has occurred.**
Please try again by launching the app installed on your site.
After a lot of searching and understanding the code of sharepoint context class, I got to know that every POST request must have SPHostURL appended in its query string, which was strange because another project that I was working on in my company (a sharepoint hosted MVC app) did not require that. So I went through the code of the SharepointContext.cs of that project (since I was only looking after MVC part of the project I did not know the sharepoint part that well). They had done a very simple thing. In the first GET request they had put the spHostURl in a cookie and made a small change in the GetSPHostUrl method:
string spHostUrlString = TokenHelper.EnsureTrailingSlash(httpRequest.QueryString[SPHostUrlKey]);
if (spHostUrlString == null)
spHostUrlString = TokenHelper.EnsureTrailingSlash(httpRequest[SPHostUrlKey]);
They added a fallback code, that if the query string does not have the SPHostURL check a cookie with the same key. Since we already added the SPHostUrl in the cookie in first GET request we'll always have it present for all the posts after that. So the sharepointcontext filter attribute is not needed at all, all we need is these two lines in the GetSPHostUrl method and storing the url in a cookie.
In order to update the Vote count I needed to get the List entery from Survey Answers list for a particular question number and for a particular answer. So I started by writing a 'Caml' query with a where clause. But it did not work, may be there was something wrong with the query. Since that part was not much of importance at that point, I decided to fetch all the entries and the filter the ListItemCollection by Question number and title in c#, which worked out well. Later when the list is big this could introduce a performance problem. So I will dig into Caml queries in future. I added a ShrePointHelper class to which I added a method to fetch list items given a title. Below is the Code snippet of how I updated the Vote Count.
ListItem listItem = items != null ? items.Where(x => x["QuestionNumber"].ToString() == surveyAnswer.SurveyId
&& x["Title"].Equals(surveyAnswer.SelectedAnswer)).FirstOrDefault() : null;
if (listItem != null)
{
listItem["VoteCount"] = Convert.ToInt32(listItem["VoteCount"]) + 1;
listItem.Update();
clientContext.ExecuteQuery();
}
By the end of this day, I was able to update the vote count in the list and I made successful checkin in the GIT repository.
Added the web app as an app part in sharepoint.
-
In the solution added a new item in the sharepoint project. Selected Office/Sharepoint in the types and then selected 'Client Web Part (Host Web)' named it as AppPollWebPart
-
On the next screen set Url for the app part, since I wanted to load Survey controller's View, I put below Url
-
Then I pressed F5 which re-installed the app with new changes and asked to verify if I 'trust it' again.
-
After that I went to the page where I wanted to install the app part and in the Page menu tab, I hit Edit. Then I click on insert and select App Part:
There in the list I could see the Client Web part which I added in the sharepoint App (AppPollWebPart). I selected that hit add and Voila! Its added on the page. Hit save to save the page and my web app is rendered in the little Iframe on the page. I could see the main view in that Iframe. The app worked perfectly.
-
Further I made changes in the appearance of the app part by editing it. Here are the screen caps for that.
I have been making commits regularly but haven't had time to write an update. So here goes the update for the last week.
Things done:
- Survey Chart View
- Signal R coding to notify Updated votes to the survey chart view
- Deployed the sharepoint app to azure.
- Created a new separate page in the site collection to add the app parts
- Created a new user for the sharepoint tenant
- Added permissions to allow the new user to access the site collection which has the page for the app parts
Details:
-
Survey Chart View
Used HighCharts to display the pie chart for showing the current votes status. A partial view to display the chart, with a next button on the main view to navigate to the next chart. Nothing major to note here.
-
This did not work (which is copied from the site)
$('#container').highcharts
This did
chart = new Highcharts.Chart({ chart: { renderTo: <theId> } })
-
Created a new model SurveyChartViewModel to feed the data to Highcharts in the correct format
public class SurveyChartViewModel { public string SurveyId { get; set; } public string Question { get; set; } public List<AnswerViewModel> Answers { get; set; } } public class AnswerViewModel { public string name { get; set; } public double y { get; set; } public bool sliced { get; set; } public bool selected { get; set; } }
- In order to feed the answers list in Json format to the highcharts function used below code:
data: @Html.Raw(Json.Encode(Model.Answers.ToArray())),
-
-
Signal R code for communication between app parts
There are many tutorials for creating a chat application in MVC using signalR. I wanted to do something similar. I wanted the app part that displays the survey results chart to be updated at the instant someone submits a vote i the other app part.So one app part was the sender and one receiver, unlike a chat app where both the clients act as senders and receivers. Here are the steps to follow to achieve that:
1. Install the Nuget package Asp .Net SignalR from package manager or from command line
2. Add an OWIN startup class file:
Select Add new Item to project. Then select OWIN Startup Class. Put below code in the Configuration function
```
app.Map("/signalr", map =>
{
// Setup the CORS middleware to run before SignalR.
// By default this will allow all origins. You can
// configure the set of origins and/or http verbs by
// providing a cors options with a different policy.
var hubConfiguration = new HubConfiguration
{
// You can enable JSONP by uncommenting line below.
// JSONP requests are insecure but some older browsers (and some
// versions of IE) require JSONP to work cross domain
// EnableJSONP = true
};
// Run the SignalR pipeline. We're not using MapSignalR
// since this branch already runs under the "/signalr"
// path.
map.RunSignalR(hubConfiguration);
});
```
3. Add signalR Hub class:
Select add new item to the project. Then select **SignalR Hub Class**
Add below function in the class. This is a server method in which we call the client method on the all/ specific clients.
```
public void notifyVotes(string user, SurveyModel data)
{
Clients.All.showNotifiedVotes(user,data);
}
```
So here _notifyVotes_ is a server method which calls _showNotifiedVotes_ method defined on all clients.
4. Code for sender
The sender needs to call the server method of the hub class with the data that is to be sent to the clients. This is done in a handler that should trigger this broadcast. In our case that would be the Vote button click handler. So here goes the code for that:
```
$(document).ready(function() {
$.connection.hub.start().done(function () {
$('.submitVote').click(function () {
var surveyId = $(".survey:Visible").attr('id');
var currentSurveyDiv = $(".survey#" + surveyId);
var selectedAnswer = $(currentSurveyDiv.find($("input[name='SelectedAnswer']:checked"))).val();
$.ajax({
type: 'POST',
data: { SurveyId: surveyId, SelectedAnswer: selectedAnswer },
url: "Survey/SubmitVote",
success: function (data, textStatus, jqXHR) {
// Call the Send method on the hub.
var theHub = $.connection.theHub;
theHub.server.notifyVotes(data.user, data.data);
},
error: function (jqXHR, textStatus, errorThrown) {
console.log('Error: ' + errorThrown);
}
});
});
});
});
```
So we define the handle for submit vote button.Important thing to remember is the handler must be registered after the connection start method of the hub is 'done' or succeeded. So the jquery click handler is put in the call back function of the start method. As the ajax call that updates the votes in SharePoint list succeeds the server method in the hub is called with the Id of the survey that was updated and the user who updated it.
5. Code for receiver
```
$(document).ready(function() {
$.connection.hub.start();
});
function showNotifiedVotes(user, data) {
//make ajax call to get particular survey chart
var surveyId = data.SurveyId;
$.ajax({
type: 'GET',
data: { SurveyId: surveyId },
url: "SurveyChart/GetSurveyChart",
success: function (data, textStatus, jqXHR) {
$(".surveyChartWrapper#" + surveyId).empty().html(data);
$(".surveyChartWrapper:Visible").hide();
$(".surveyChartWrapper#" + surveyId).show();
var nextSurveyId = parseInt(surveyId) + 1;
if ($(".surveyChartWrapper#" + nextSurveyId).length > 0)
$("#btnNext").show();
else
$("#btnNext").hide();
},
error: function (jqXHR, textStatus, errorThrown) {
console.log('Error: ' + errorThrown);
}
});
var encodedMsg = $('<div />').text(user + " just voted for " + data.SelectedAnswer ).html();
toastr.warning(encodedMsg);
```
The receiver's showNotitfiedVotes function is called from the server method of the hub and it re-renders the chart for the corresponding survey Id. Also gives a toastr notification of which user voted for which option. **Be sure to start the hub connection in document.ready event handler.**
6. Refer below scripts in both sender and receiver view.
```
<script src="~/Scripts/jquery-1.10.2.min.js"></script>
<!--Reference the SignalR library. -->
<script src="~/Scripts/jquery.signalR-2.2.0.min.js"></script>
<!--Reference the autogenerated SignalR hub script. -->
<script src="~/signalr/hubs"></script>
```
7. Combined API for all signalR javascript ops
Just to have the signalR code at one place instead of scattering it over the different views, I added one js file SignalR.API.js which has 3 methods.
1. initReceiver : contains the receiver's code. Called in doc.ready event handler of the receiver
2. initSender: contains the event handler for submit vote button
3. startConnection(callback): which starts a connection and accepts a callback function which is called in the 'done' success handler of the start method. Here is how the sender and receiver code look like now:
```
//sender (inside doc.ready event handler)
SignalR_API.startConnection(SignalR_API.initSender);
//client (inside doc.ready event handler)
SignalR_API.initReceiver();
SignalR_API.startConnection();
```
-
Deployed the sharepoint app to azure. Here are the steps to do that:
-
Create a simple web site (or with the new app services launch lets call it web app) from windows azure portal. Follow this tutorial's How to: Create a web app Using the Azure Portal section if you don't already know how to do that.
-
Download the publish profile from the web app's dashboard.
-
In Visual studio, right click the sharepoint app (not the web app) and say Publish
-
It opens up 'Publish your app' page. You won't have any Publish profiles the first time so click on New in the Current Profile drop down.
-
Import the downloaded publish profile. (here we can create new profile and skip the part above where we created the web app from windows azure portal and downloaded the profile. This wizard would do that on our behalf)
-
Put ClientId and Client Sercret on the next screen (You can generate this from the appregnew.aspx page in sharepoint tenant. But DO NOT CREATE THE APP THERE). and click finish.
-
On Azure portal, in the web app created go to the configure tab and add clientID and Client secret in the app settings as azure refers to these config entries and not the web.config.
-
In the 'Publish your App' in visual studio, click on the 'Package the app' which creates a .app file on your local disk
-
Install the app on your Sharepoint tenant.
-
Go to 'Apps in Testing'
-
Add new Item
-
Browse to the .app file created in step 8 and select it.
-
Click on Deploy
-
-
Now if you click on the installed app in 'Apps in Testing', it should navigate to the azure web site and display the app correctly.
-
-
Created a new separate page in the site collection to add the app parts
- Click on settings icon in right upper corner, select add a page. Follow the wizard
- Follow the steps described above to add the app parts to the page. These app parts are now rendered from the azure website.
-
Created a new user for the sharepoint tenant
- Click on app tray icon in left upper corner and select admin. It opens Office 365 admin center.
- On Right side, in admin shortcuts select create new user. Follow the wizard
-
Added permissions to allow the new user to access the site collection which has the page for the app parts
- Click on app tray icon in left upper corner and select admin. It opens Office 365 admin center.
- In left section (list) expand Admin and click on sharepoint. It opens SharePoint admin center.
- Select the site collection in which we have added the app parts.
- Click on Owner in tools tray, and select manage administrators.
- Add the newly created user in the administrators