Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Bendik Engebretsen 104 posts 196 karma points
    Nov 02, 2016 @ 20:41
    Bendik Engebretsen
    5

    File upload in backoffice custom section

    While trying to implement a file upload form in a backoffice custom section, I stumbled onto quite a few obstacles and outdated/incorrect information in various forums (not only in Our;-) So, after I finally got it to work, I thought I'd share my solution and code here for anyone else interested.

    First of all, you need to have your backoffice custom section up and running. There's a good article about how to do one here. From here, I will assume that you have one called "MySection".

    The next thing you'll need is the ng-file-upload AngularJS directive from danialfarid. If you're in Visual Studio, there's a NuGet package called "angular-file-upload" which will install the .js files. Otherwise, get them from GitHub here. Ensure that you have the ng-file-upload-all.js file in your ~/Scripts folder.

    ng-file-upload has a lot of functionality (drag-drop, etc.), some of which I may explore later, but for now I opted for a very simple file input control and an upload button. So in your edit.html markup for your custom section something like this:

                    <div class="umb-pane">
    
    
                    <umb-control-group label="File" description="File to upload">
                        <input type="file" class="umb-editor umb-textstring" ngf-select="" ng-model="files" ng-multiple="false" ngf-change="fileSelected(files)" required />
                    </umb-control-group>
    
                    <div class="umb-tab-buttons" detect-fold>
                        <div class="btn-group">
                            <button type="button" data-hotkey="ctrl+s" class="btn btn-success" ng-click="uploadFile()">
                                Upload
                            </button>
                        </div>
                    </div>
                </div>
    

    Note the use of the ng- and ngf- attributes. When you select a file (only one in my case), fileSelected(files) is called to store the local path and filename in the $scope. So that when the Upload button is clicked, and fileUpload() is called, it knows what file(s) to upload. I chose to put these two in the edit.controller.js for my section, so add this code:

        $scope.fileSelected = function (files) {
            // In this case, files is just a single path/filename
            $scope.file = files;
        };
    
        $scope.uploadFile = function () {
            if (!$scope.isUploading) {
                if ($scope.file) {
                    $scope.isUploading = true;
                    fileUploadService.uploadFileToServer($scope.file)
                        .then(function (response) {
                            if (response) {
                                notificationsService.success("Success", "Saved to server with the filename " + response);
                            }
                            $scope.isUploading = false;
                        }, function (reason) {
                        notificationsService.error("Error", "File import failed: " + reason.message);
                        $scope.isUploading = false;
                    });
                } else {
                    notificationsService.error("Error", "You must select a file to upload");
                    $scope.isUploading = false;
                }
            }
        };
    
        $scope.file = false;
        $scope.isUploading = false;
    

    Now we need the fileUploadService, which I chose to put in a separate file called file.upload.api.service.js:

    angular.module("umbraco.resources")
    .factory("fileUploadService", function ($http) {
        return {
            uploadFileToServer: function (file) {
                var request = {
                    file: file
                };
                return $http({
                    method: 'POST',
                    url: "backoffice/MySection/MySectionApi/UploadFileToServer",
                    headers: { 'Content-Type': undefined },
                    transformRequest: function (data) {
                        var formData = new FormData();
                        formData.append("file", data.file);
                        return formData;
                    },
                    data: request
                }).then(function (response) {
                    if (response) {
                        var fileName = response.data;
                        return fileName;
                    } else {
                        return false;
                    }
                });
            }
        };
    });
    

    To make sure the ng-file-upload-all.js and file.upload.api.service.js are loaded, we need to list them in the package.manifest for our section:

    {
    javascript: [
        '~/App_Plugins/MySection/backoffice/mySectionTree/edit.controller.js',
        '~/App_Plugins/MySection/mySection.resource.js',
        '~/App_Plugins/MySection/file.upload.api.service.js',
        '~/Scripts/ng-file-upload-all.js'
    ]}
    

    Next thing is to implement the actual uploading. I put it in my api controller for the section, i.e. MySectionApiController.cs. Add the following method to your MySectionApiController class:

        public async Task<HttpResponseMessage> UploadFileToServer()
        {
            if (!Request.Content.IsMimeMultipartContent())
            {
                throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
            }
    
            string uploadFolder = HttpContext.Current.Server.MapPath("~/App_Data/FileUploads");
            Directory.CreateDirectory(uploadFolder);
            var provider = new CustomMultipartFormDataStreamProvider(uploadFolder);
            var result = await Request.Content.ReadAsMultipartAsync(provider);
            var fileName = result.FileData.First().LocalFileName;
            HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK);
            response.Content = new StringContent(fileName);
            return response;
        }
    

    To override the generated filename, I also added this class (yes, I know this may have a security impact):

    public class CustomMultipartFormDataStreamProvider : MultipartFormDataStreamProvider
    {
        public CustomMultipartFormDataStreamProvider(string path) : base(path) { }
    
        public override string GetLocalFileName(HttpContentHeaders headers)
        {
            return headers.ContentDisposition.FileName.Replace("\"", string.Empty);
        }
    }
    

    And that's about it! (Hope I didn't forget anything. If someone tests it and finds that I did, please don't hesitate to let me know!)

  • Tim Geyssens 6419 posts 15026 karma points MVP 2x c-trib
    Nov 02, 2016 @ 20:46
    Tim Geyssens
    0

    Thanks for sharing :)

  • Sean Maloney 4 posts 74 karma points
    Nov 07, 2016 @ 10:03
    Sean Maloney
    0

    Hi Bendik,

    Thanks for posting this - had been trying to figure out how to approach it and now got it working after following this :-)

    I think there's a typo in the view:

    <button type="button" data-hotkey="ctrl+s" class="btn btn-success" ng-click="**fileUpload**()">
    

    Should be:

    <button type="button" data-hotkey="ctrl+s" class="btn btn-success" ng-click="**uploadFile**()">
    

    Thanks again, Sean.

  • Bendik Engebretsen 104 posts 196 karma points
    Nov 09, 2016 @ 09:31
    Bendik Engebretsen
    0

    Thanks, Sean! I have now updated the code in the original post. Glad to hear that my post was helpful.

  • djscorch 67 posts 106 karma points
    Jan 19, 2017 @ 11:26
    djscorch
    0

    We get the following error when selecting the file:

    Uncaught InvalidStateError: Failed to set the 'value' property on 'HTMLInputElement': This input element accepts a filename, which may only be programmatically set to the empty string
    

    Our implementation pretty much mirrors what you have above.

    Any ideas?

  • Bendik Engebretsen 104 posts 196 karma points
    Feb 07, 2017 @ 00:27
    Bendik Engebretsen
    0

    Tough one, but I think the error message indicates that your code (or someone else's) is somehow trying to modify the value of the <input type="file"...> element, which is not allowed. And this happens just after you've selected a file? In that case, it could seem that the offending code is

    $scope.fileSelected = function (files) {
        // In this case, files is just a single path/filename
        $scope.file = files;
    };
    

    But I fail to see what could be wrong with that simple function. This is probably a shot in the dark, but how about changing the name of the $scope.file variable in edit.controller.js to say $scope.theFile?

    Otherwise it could be something in the ng-file-upload directive that isn't cooperating well with your code/markup. But that's a huge chunk of js code to understand/debug, and it does seem to be very well tested and up-to-date. So in that case I guess you should try to work your way around it. Try various mods to your markup and controller and see if anything changes. There is also a forum for issues in ng-file-upload: https://github.com/danialfarid/ng-file-upload/issues. If you're really stuck it might be worth trying a post there.

  • hank 2 posts 92 karma points
    Feb 24, 2017 @ 18:20
    hank
    0

    Hi Bendik,

    I am trying to implement your version of this file upload functionality.

    When testing I can see the file input and the upload button but I get a javascript error saying:

    Uncaught ReferenceError: $scope is not defined
    

    Any chance you can help me out here or provide me a download link for the files implemented by you? I'm working on v7.5.6.

  • hank 2 posts 92 karma points
    Feb 26, 2017 @ 22:43
    hank
    100

    You can ignore the $scope error I posted before. I just copied the controller code as is in the controller.js file. But that was obvisously not enough, I forgot to put the controller declaration around it. So it should be something like this:

    angular.module("umbraco").controller("MyControllerName",
    function MyImportController($scope, $routeParams, $http, fileUploadService) {
        $scope.fileSelected = function (files) {
            // In this case, files is just a single path/filename
            $scope.file = files;
        };
        $scope.uploadFile = function () {
            if (!$scope.isUploading) {
                if ($scope.file) {
                    $scope.isUploading = true;
                    fileUploadService.uploadFileToServer($scope.file)
                        .then(function (response) {
                            if (response) {
                                notificationsService.success("Success", "Saved to server with the filename " + response);
                            }
                            $scope.isUploading = false;
                        }, function (reason) {
                            notificationsService.error("Error", "File import failed: " + reason.message);
                            $scope.isUploading = false;
                        });
                } else {
                    notificationsService.error("Error", "You must select a file to upload");
                    $scope.isUploading = false;
                }
            }
        };
        $scope.file = false;
        $scope.isUploading = false;
    });
    

    Now it works as it should be. Thank you!

  • Bendik Engebretsen 104 posts 196 karma points
    Mar 16, 2017 @ 13:09
    Bendik Engebretsen
    0

    Sorry for being late following up here, but you solved it already, great! I'm sure this will be helpful to others.

  • Sean Hynes 10 posts 142 karma points
    Mar 25, 2017 @ 16:03
    Sean Hynes
    0

    Hi

    New to umbraco and also angular js - but this post has been useful. However, struggling with getting it to work if anyone can help

    I've using virtually an exact copy of the code above.

    1) When I bind the controller to my edit.html file I seem to be losing the "choose file" section - the code in umb-control-group. It just disappears and I'm left with the upload button and the code I inserted stating "start of loading section". Here's my edit.html code for my backoffice section:

    <form name="contentForm"
      data-ng-controller="MyControllerName"
      ng-show="loaded"
      val-form-manager>
    <div>
            <div class="spare">Start of loading section</div>
            <div class="umb-pane">
                <umb-control-group label="File" description="File to upload">
                    <input type="file" class="umb-editor umb-textstring" ngf-select="" ng-model="files" ng-multiple="false" ngf-change="fileSelected(files)" required />
                </umb-control-group>
    
                <div class="umb-tab-buttons" detect-fold>
                    <div class="btn-group">
                        <button type="button" data-hotkey="ctrl+s" class="btn btn-success" ng-click="uploadFile()">
                            Upload here
                        </button>
                    </div>
                </div>
            </div>
    </div>
    

    Unbinding the controller - just deleting the data-ng-controller brings back the choose section.

    Hoping someone can help as no idea why I can't seem to bind the controller?

    Regards Sean

  • Pagggy 15 posts 34 karma points
    Jan 24, 2019 @ 14:24
    Pagggy
    0

    Thanks for sharing. Saved me tons of time.

Please Sign in or register to post replies

Write your reply to:

Draft