Uploading Files to Sitefinity Asynchronously Using Kendo UI Upload
How To Upload Files to Sitefinity Asynchronously with Kendo UI Upload widget
Kendo UI Upload is a great jQuery widget that is able to upload files asynchronously using the HTML5 File API and it also provides fallback for legacy browsers.
In general, you need to configure save action that will receive the uploaded files. An optional remove action is also available. Let's see how this can be done in Sitefinity.
Goal
Create a front-end widget that will allow a logged in user to upload one or more files asynchronously to a Sitefinity Document Library.
Implementation
I'll use Sitefinity Feather to create an ASP.NET MVC widget.
Controller
The main controller itself will be very simple - it will just register the widget to the Page Toolbox (via the ControllerToolboxItem attribute) and return the View where the client side code will reside.
using
System.Web.Mvc;
using
Telerik.Sitefinity.Mvc;
namespace
SitefinityWebApp.Mvc.Controllers
{
[ControllerToolboxItem(Name =
"FileAsyncUploader"
, Title =
"File Async Uploader"
, SectionName =
"Custom MVC"
, CssClass =
"sfMvcIcn"
)]
public
class
FileAsyncUploaderController: Controller
{
[HttpGet]
public
ActionResult Index()
{
return
View(
"Index"
);
}
[HttpPost]
public
ActionResult Upload()
{
// save the file to Sitefinity
return
new
EmptyResult();
}
}
}
Fig: FileAsyncUploaderController.cs in /Mvc/Controllers folder
It is pretty standard, but you may wonder why the Upload method is not doing anything and why it returns an empty result?
The answer is this: when Kendo UI Upload sends the files to the server it requires an empty response to signify success of the operation. If it does not get an empty response, it will show the files in red, even if you have successfully saved them:
Figure: if the server returns a non-empty response, Kendo UI Upload thinks there was an error.
Ok, but you will say - hey, you do return an EmptyResult from your controller, what is the issue then?
The issue is that if you try to send a request to a controller in Sitefinity directly, the request goes through the Sitefinity Pages Http handler and even if the controller's action return an empty result - Sitefinity thinks you are actually browsing the page that contains this controller, so it will return you the whole html of the page.
Here is the evidence:
Figure: if you post to a controller in Sitefinity asynchronously, even if the controller's action return an empty result, Sitefinity will add the full page html as it thinks you are browsing the page where the controller is.
That's why we have to post the files not to the main controller, but to an ApiController.
An ApiController can return any response and Sitefinity will not interfere with it at all.
First, let's register a custom route:
Global.asax.cs
And next let's create a new folder called Api under the /MVC folder and put the ApiController there:
/Mvc/Api/FilesController.cs
It may look like a lot of code, but most of it is just Sitefinity infrastructure.
Let's break it down a bit: the Upload method is what is being requested by the "save" method of Kendo UI Upload. We check if the user is authenticated and if not return an error.
Next we loop through the Files collection and create a Sitefinity document for each of them. We also create a library (if it does not exist yet) with the name of the currently logged in user. If all went well, the error string would be empty and would be returned as a result to Kendo Upload which will show the items as successfully uploaded:
Fig.: Kendo Upload shows that the files have been uploaded successfully
View
The Index.cshtml view includes the KendoWeb javascript that comes with Sitefinity and also loads the 2 bootstrap theme Kendo files from Telerik's CDN
The saveUrl is our Upload action in the ApiController and we also have a removeUrl which goes to the corresponding Remove action.
autoUpload: true means the files will start uploading as soon as the user selects them.
batch: true means that all files selected at once will be sent in a single request.
That view has a dependency on jQuery, so it is assumed that jQuery is loaded by the head section of the main page Layout template.
And here are the uploaded files:
The gist with all code is here:
https://gist.github.com/VesselinVassilev/bb040081ccd9868853a47a9e5d6152e3
It is pretty standard, but you may wonder why the Upload method is not doing anything and why it returns an empty result?
The answer is this: when Kendo UI Upload sends the files to the server it requires an empty response to signify success of the operation. If it does not get an empty response, it will show the files in red, even if you have successfully saved them:
Figure: if the server returns a non-empty response, Kendo UI Upload thinks there was an error.
Ok, but you will say - hey, you do return an EmptyResult from your controller, what is the issue then?
The issue is that if you try to send a request to a controller in Sitefinity directly, the request goes through the Sitefinity Pages Http handler and even if the controller's action return an empty result - Sitefinity thinks you are actually browsing the page that contains this controller, so it will return you the whole html of the page.
Here is the evidence:
Figure: if you post to a controller in Sitefinity asynchronously, even if the controller's action return an empty result, Sitefinity will add the full page html as it thinks you are browsing the page where the controller is.
That's why we have to post the files not to the main controller, but to an ApiController.
An ApiController can return any response and Sitefinity will not interfere with it at all.
First, let's register a custom route:
Global.asax.cs
using
System;
using
System.Linq;
using
System.Web.Hosting;
using
System.Web.Http;
using
System.Web.Optimization;
using
System.Web.Routing;
using
Telerik.Sitefinity.Services;
namespace
SitefinityWebApp
{
public
class
Global : System.Web.HttpApplication
{
protected
void
Application_Start(
object
sender, EventArgs e)
{
SystemManager.ApplicationStart += SystemManager_ApplicationStart;
}
private
void
SystemManager_ApplicationStart(
object
sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
}
private
void
RegisterRoutes(RouteCollection routes)
{
routes.MapHttpRoute(
name:
"MyCustomApi"
,
routeTemplate:
"ajax/{controller}/{action}/{id}"
,
defaults:
new
{ id = RouteParameter.Optional }
);
}
}
}
And next let's create a new folder called Api under the /MVC folder and put the ApiController there:
/Mvc/Api/FilesController.cs
using
System;
using
System.Net;
using
System.Net.Http;
using
System.Web.Http;
using
Telerik.Sitefinity.Abstractions;
using
System.Linq;
using
System.Collections.Generic;
using
System.Web;
using
System.IO;
using
Telerik.Sitefinity.Libraries.Model;
using
Telerik.Sitefinity.Modules.Libraries;
using
Telerik.Sitefinity.Workflow;
using
Telerik.Sitefinity.Data;
using
System.Text.RegularExpressions;
using
Telerik.Sitefinity.Security.Claims;
using
Telerik.Sitefinity.GenericContent.Model;
namespace
SitefinityWebApp.Mvc.Api
{
public
class
FilesController : ApiController
{
[HttpPost]
public
HttpResponseMessage Upload()
{
var user = ClaimsManager.GetCurrentIdentity();
if
(!user.IsAuthenticated)
{
return
Request.CreateErrorResponse(HttpStatusCode.Forbidden,
"You must be authenticated to upload files"
);
}
var error =
string
.Empty;
var files = HttpContext.Current.Request.Files;
if
(files !=
null
)
{
var manager = LibrariesManager.GetManager();
using
(var reg =
new
ElevatedModeRegion(manager))
{
// create a library with the name of the logged in user
var libTitle = user.Name;
var library = GetOrCreateDocumentsLibrary(libTitle);
for
(
int
i = 0; i < files.Count; i++)
{
var file = files[i];
try
{
// Some browsers send file names with full path.
// This needs to be stripped.
var fileName = Path.GetFileName(file.FileName);
var ext = Path.GetExtension(file.FileName).ToLower();
var docId = Guid.NewGuid();
var doc = manager.CreateDocument(docId);
var utcNow = DateTime.UtcNow;
doc.Parent = library;
doc.Title = fileName;
doc.DateCreated = utcNow;
doc.PublicationDate = utcNow;
doc.LastModified = utcNow;
var urlName = CreateSitefinityUrlFromString(fileName);
doc.UrlName = doc.MediaFileUrlName = urlName;
manager.Upload(doc, file.InputStream, ext);
manager.RecompileItemUrls(doc);
manager.SaveChanges();
var bag =
new
Dictionary<
string
,
string
>();
bag.Add(
"ContentType"
,
typeof
(Document).FullName);
WorkflowManager.MessageWorkflow(docId,
typeof
(Document),
null
,
"Publish"
,
false
, bag);
}
catch
(Exception ex)
{
Log.Write(ex.ToString());
error =
"Failed to upload: "
+ file.FileName + ex.ToString();
}
}
}
}
// Return an empty string to signify success or an error message otherwise
return
Request.CreateResponse(HttpStatusCode.OK, error);
}
[HttpPost]
public
HttpResponseMessage Remove([FromBody] RemoveFileModel filesToRemove)
{
var user = ClaimsManager.GetCurrentIdentity();
if
(!user.IsAuthenticated)
{
return
Request.CreateErrorResponse(HttpStatusCode.Forbidden,
"You must be authenticated to upload files"
);
}
var manager = LibrariesManager.GetManager();
using
(var region =
new
ElevatedModeRegion(manager))
{
var library = manager.GetDocumentLibraries()
.Where(l => l.Title == user.Name)
.Single();
var fileNames = filesToRemove.fileNames;
var docToDelete = library.Documents()
.Where(d => d.Status == ContentLifecycleStatus.Master)
.Where(d => fileNames.Contains(d.Title.ToString()))
.ToList();
docToDelete.ForEach(d => manager.DeleteDocument(d));
manager.SaveChanges();
}
return
Request.CreateResponse(HttpStatusCode.OK);
}
public
Library GetOrCreateDocumentsLibrary(
string
title)
{
var librariesManager = LibrariesManager.GetManager();
Library library =
null
;
using
(var region =
new
ElevatedModeRegion(librariesManager))
{
var libs = librariesManager.GetDocumentLibraries().Where(l => l.Title == title);
if
(libs.Count() == 0)
{
library = librariesManager.CreateDocumentLibrary();
library.Title = title;
library.DateCreated = DateTime.UtcNow;
library.LastModified = DateTime.UtcNow;
library.UrlName = CreateSitefinityUrlFromString(title);
librariesManager.RecompileAndValidateUrls(library);
librariesManager.SaveChanges();
}
else
{
library = libs.First();
}
}
return
library;
}
public
string
CreateSitefinityUrlFromString(
string
title,
bool
isTitleNotUnique =
false
)
{
var url = Regex.Replace(title.ToLower(), @
"[^\w\-\!\$\'\(\)\=\@\d_]+"
,
"-"
);
if
(isTitleNotUnique)
{
url += Guid.NewGuid().ToString();
}
return
url;
}
}
public
class
RemoveFileModel
{
// the name of the files to be deleted
public
string
[] fileNames {
get
;
set
; }
}
}
It may look like a lot of code, but most of it is just Sitefinity infrastructure.
Let's break it down a bit: the Upload method is what is being requested by the "save" method of Kendo UI Upload. We check if the user is authenticated and if not return an error.
Next we loop through the Files collection and create a Sitefinity document for each of them. We also create a library (if it does not exist yet) with the name of the currently logged in user. If all went well, the error string would be empty and would be returned as a result to Kendo Upload which will show the items as successfully uploaded:
Fig.: Kendo Upload shows that the files have been uploaded successfully
View
The Index.cshtml view includes the KendoWeb javascript that comes with Sitefinity and also loads the 2 bootstrap theme Kendo files from Telerik's CDN
@using Telerik.Sitefinity.Frontend.Mvc.Helpers;
@using Telerik.Sitefinity.Modules.Pages;
@Html.Script(ScriptRef.KendoWeb, "bottom")
@Html.StyleSheet("//kendo.cdn.telerik.com/2017.2.504/styles/kendo.common-bootstrap.min.css", "head")
@Html.StyleSheet("//kendo.cdn.telerik.com/2017.2.504/styles/kendo.bootstrap.min.css", "head")
<
div
class
=
"row"
>
<
div
class
=
"col-md-4"
>
<
div
class
=
"form-group"
>
<
label
>Upload Files</
label
>
<
input
name
=
"files"
id
=
"files"
type
=
"file"
/>
</
div
>
</
div
>
</
div
>
<
script
>
$(function() {
var saveUrl = '/ajax/files/upload';
var removeUrl = '/ajax/files/remove';
$("#files").kendoUpload({
multiple: true,
async: {
saveUrl: saveUrl,
removeUrl: removeUrl,
autoUpload: true,
batch: true
}
});
})
</
script
>
The saveUrl is our Upload action in the ApiController and we also have a removeUrl which goes to the corresponding Remove action.
autoUpload: true means the files will start uploading as soon as the user selects them.
batch: true means that all files selected at once will be sent in a single request.
That view has a dependency on jQuery, so it is assumed that jQuery is loaded by the head section of the main page Layout template.
And here are the uploaded files:
The gist with all code is here:
https://gist.github.com/VesselinVassilev/bb040081ccd9868853a47a9e5d6152e3