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 105 posts 198 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 6562 posts 15373 karma points MVP 2x c-trib
    Nov 02, 2016 @ 20:46
    Tim Geyssens
    0

    Thanks for sharing :)

  • Sean Maloney 4 posts 77 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 105 posts 198 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 105 posts 198 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 105 posts 198 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.

  • eugenewo 1 post 71 karma points
    Jan 12, 2020 @ 15:06
    eugenewo
    0

    do you have the full working demo? cant make it work

  • 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 27 posts 46 karma points
    Jan 24, 2019 @ 14:24
    Pagggy
    0

    Thanks for sharing. Saved me tons of time.

  • Arjan H. 142 posts 275 karma points
    May 31, 2020 @ 17:33
    Arjan H.
    0

    For those of you who are struggling with file uploads in custom sections in Umbraco 8 here's my solution:

    View:

    <input type="file" class="umb-textstring" ngf-select="" ng-model="files" ng-multiple="false" ngf-change="fileSelected(files)" required />
    <umb-button action="vm.clickUploadButton()"
                type="button"
                button-style="info"
                label="Upload">
    </umb-button>
    

    Controller:

    function Controller($scope, Upload) {
        var vm = this;
        vm.clickUploadButton = clickUploadButton;
    
        $scope.fileSelected = function (files) {
            $scope.file = files;
        };
    
        function clickUploadButton() {
            if (!$scope.file)
                return false;
    
            Upload.upload({
                url: "backoffice/api/Upload/UploadFile",
                fields: {
                    "field1": "value1",
                    "field2": "value2"
                },
                file: $scope.file
            }).success(function (data, status, headers, config) {
                console.log(status);
                console.log(data);
            }).error(function (evt, status, headers, config) {
                console.log(evt.Message);
            });
        }
    }
    

    API controller:

    public class UploadController : UmbracoAuthorizedApiController
    {
        [HttpPost]
        public HttpResponseMessage UploadFile()
        {
            var files = HttpContext.Current.Request.Files;
            var field1 = HttpContext.Current.Request.Form["field1"];
            var field2 = HttpContext.Current.Request.Form["field2"];
    
            return Request.CreateResponse(HttpStatusCode.OK, new { fileCount = files.Count, field1, field2 }) ;
        }
    }
    
  • This forum is in read-only mode while we transition to the new forum.

    You can continue this topic on the new forum by tapping the "Continue discussion" link below.

Please Sign in or register to post replies