Wyświetlanie profili podejście pierwsze

Po długich bojach ze wstępną konfiguracją aplikacji czas zacząć rozwijać po kolei funkcjonalności, które mam zaplanowane w pierwszej fazie Social Cooking. Na pierwszy ogień idzie wyświetlanie profili. Dlaczego akurat to? Ze względu na to, że moduł ten będzie stosunkowo prosty w implementacji, ale będzie zawierał sporo ciekawych rozwiązań, które przydadzą się w późniejszej fazie. Zatem żeby nie przedłużać, zaczynam.

Założenia

Informacje dotyczące profili użytkowników od strony bazy danych będą składały się z dwóch tabel.

  • standardowa tabela .NET Identities AspNetUsers. Zawiera ona podstawowe informacje dot. użytkownika takie jak email, numer telefonu, czy nazwę użytkownika
  • powiązana z nią tabela UsersExtensions zawierająca datę urodzenia, datę dołączenia, dane adresowe, czy ścieżkę do zdjęcia profilowego.

Zdjęcia profilowe (jak i każde inne pliki graficzne) będą przechowywane fizycznie na serwerze gdzie hostowane będzie WebAPI, natomiast w bazie będą jedynie ścieżki do tych plików.

users-dbdiagram

Implementacja WebAPI

Po stronie WebAPI stworzyłem nowy ProfileController, którego najważniejsze metody obsługujące zadania to:

  • GetProfileById
  • UploadImage

Wydaje mi się, że nie muszę tłumaczyć co będą robiły 🙂

public async Task<HttpResponseMessage> GetProfileById(string Id)
        {
            UsersExtension user = await _usersRepo.GetUsersExtensionByIdentityIdAsync(Id);
            Mapper.CreateMap<UsersExtension, ProfileViewModel>();
            ProfileViewModel profileVm = Mapper.Map<UsersExtension, ProfileViewModel>(user);

            return Request.CreateResponse(HttpStatusCode.OK, profileVm);
        }

Pobranie profilu jak widać jest sprawą banalnie prostą. Odwołuję się asynchronicznie do repozytorium skąd zwracam obiekt UsersExtension, który potem mapuję na prosty ViewModel (który jedynie posiada właściwości, które będę wyświetlał). Ot i cała filozofia.

Zdecydowanie więcej się dzieje w drugiej metodzie.

[HttpPost]
        public async Task<HttpResponseMessage> UploadImage()
        {
            if (!Request.Content.IsMimeMultipartContent())
            {
                this.Request.CreateResponse(HttpStatusCode.UnsupportedMediaType);
            }
            var uploadFolder = "~/Resource/ImageFiles";
            var provider = GetMultipartProvider(uploadFolder);
            var result = await Request.Content.ReadAsMultipartAsync(provider);

            var originalFileName = GetDeserializedFileName(result.FileData.First());

            var uploadedFileInfo = new FileInfo(result.FileData.First().LocalFileName);

            var username = GetFormData(result);
            var destDir = uploadFolder + "/" + username;
            var destPath = destDir + "/" + originalFileName;


            var destLocalDir = HttpContext.Current.Server.MapPath(destDir);
            var destLocalPath = HttpContext.Current.Server.MapPath(destPath);
            string host = Request.GetRequestContext().Url.Request.Headers.Host;
            var destThumbPath = destDir + "/" + originalFileName.Split('.')[0] + "_thumb." +
                                originalFileName.Split('.')[1];

            var destLocalThumbPath = HttpContext.Current.Server.MapPath(destThumbPath);

            Directory.CreateDirectory(destLocalDir);
            File.Move(uploadedFileInfo.FullName, destLocalPath);
            ResizeSettings settings = new ResizeSettings("width=100&height=100");
            ImageBuilder builder = ImageBuilder.Current;

            builder.Build(destLocalPath, destLocalThumbPath, settings);
            var user = await
                _usersRepo.SaveProfileImagePath(username, destPath.Replace("~", "http://"+host), destThumbPath.Replace("~", "http://" + host));
            var returnData = "ImageUploaded";
            return this.Request.CreateResponse(HttpStatusCode.OK, new { returnData });
        }

        private MultipartFormDataStreamProvider GetMultipartProvider(string uploadFolder)
        {
            var root = HttpContext.Current.Server.MapPath(uploadFolder);
            Directory.CreateDirectory(root);
            return new MultipartFormDataStreamProvider(root);
        }

        private string GetFormData(MultipartFormDataStreamProvider result)
        {
            if (result.FormData.HasKeys())
            {
                if (result.FormData.AllKeys.Any(p => p == "username"))
                {
                    return result.FormData.GetValues(0).FirstOrDefault();
                }
            }

            return string.Empty;
        }

        private string GetDeserializedFileName(MultipartFileData fileData)
        {
            var fileName = GetFileName(fileData);
            return JsonConvert.DeserializeObject(fileName).ToString();
        }

        public string GetFileName(MultipartFileData fileData)
        {
            return fileData.Headers.ContentDisposition.FileName;
        }

Trochę chaotycznie mi to wyszło, ale na refactoring jeszcze przyjdzie czas 🙂 Najważniejsze póki co, że działa. Tylko właściwie co działa?

MultipartFormDataStreamProvider

Szukając sposobów na to jak zapisywać pliki przesłane z Angulara do WebAPI trafiłem na takie coś jak MultipartFormDataStreamProvider. Okazuje się, że jest to biblioteka służąca do zapisywania plików przesłanych przez Request Htmlowy. Pliki zapisują się w żądanej lokalizacji pod ciekawą nazwą „Bodypart_GUID”. Wszystko działa szybko, sprawnie i bezboleśnie. Ale mimo wszystko zapisywanie w takiej formie jest dla mnie trochę nieładne.

Dlatego też po zapisaniu tworzę folder o nazwie użytkownika i przenoszę tam ten plik nadając mu jego oryginalną nazwę.

ImageResizer

Oprócz oryginalnego pliku zapisuję również jego miniaturkę. Oczywiście do tego też jest już gotowa biblioteka. ImageResizer jest częścią większego oprogramowania, ale na moje potrzeby na szczęście jest darmowy. Całość magii kryje sie w 3 linijkach kodu:

ResizeSettings settings = new ResizeSettings("width=100&height=100");

ImageBuilder builder = ImageBuilder.Current;
builder.Build(destLocalPath, destLocalThumbPath, settings);

Wystarczy wywołać ImageBuilder ze ścieżką oryginalną, ścieżką do miniaturki i ustawieniami. Piękne i proste. Takie rozwiązania lubię 🙂

 

Implementacja AngularJS

angular-profiles-tree

Do implementacji rozwiązania po stronie Angulara potrzebuję 3 nowych:

  • kontrolera
  • serwisu
  • widoku

Oprócz tego skorzystam jeszcze z biblioteki ng-file-upload służącej do niczego innego jak wspomagania uploadowania plików.

'use strict';
app.factory('profileService', ['$http', 'ngAuthSettings', function ($http, ngAuthSettings) {
    var serviceBase = ngAuthSettings.apiServiceBaseUri;

    var profileService = {};

    var getProfileById = function (userId) {

        return $http.get(serviceBase + 'api/profile/' + userId).then(function (results) {
            return results;
        });
    };
   

    profileService.getProfileById = getProfileById;

    return profileService;

}]);

Serwis na chwilę obecną ma tylko jedno zadanie. Zapukać do WebAPI i poprosić o informacje dotyczące użytkownika.

<div class="view indent">
    <div class="container">
        <div class="button" ngf-select="vm.upload($file)">Upload on file select</div>
        <div class="profile row col-lg-12">
            <div class="profile-image col-lg-6">
                <img src={{vm.profile.imageThumbPath}} />
            </div>
            <div class="profile-title col-lg-6">
                <h3>{{vm.profile.usersUserName}}</h3>
            </div>
        </div>

        <div class="profile-birthDate row col-lg-12">
            {{vm.profile.birthDate}}
        </div>
        <div class="profile-joinDate row col-lg-12">
            {{vm.profile.joinDate}}
        </div>
        <div class="profile-city row col-lg-12">
            {{vm.profile.city}}
        </div>
        <div class="profile-country row col-lg-12">
            {{vm.profile.country}}
        </div>
    </div>
</div>

Widok jak to widok. Wyświetla informacje jakie udało się wyciągnąć z WebAPI. Do tego wykorzystanie na górze ng-file-uploadera poprzez komendę ngf-select.

&lt;br data-mce-bogus="1"&gt;
'use strict';
app.controller('profileDetailsController', ['$routeParams', '$scope', 'authService', 'profileService', 'Upload', 'ngAuthSettings', function ($routeParams, $scope, authService, profileService, Upload, ngAuthSettings) {
    var vm = this;
    vm.authentication = authService.authentication;
    var profileId = $routeParams;
    vm.profile = {};
     

    function init() {
        profileService.getProfileById(vm.authentication.userName).then(function(results) {
            vm.profile = results.data;
        }, (function(data, status, headers, config) {
            alert(data + " status: " + status);
        }));
    }

    init();

    vm.upload = function (file) {Upload.upload({
        url: ngAuthSettings.apiServiceBaseUri + '/api/profile/UploadImage',
        method: "POST",
        file: file,
        data: { "username": vm.authentication.userName }
    }).progress(function(evt) {
        // get upload percentage
        console.log('percent: ' + parseInt(100.0 * evt.loaded / evt.total));
    }).success(function(data, status, headers, config) {
        // file is uploaded successfully
        console.log(data);
        profileService.getProfileById(vm.authentication.userName).then(function (results) {
            vm.profile = results.data;
        }, (function (data, status, headers, config) {
            alert(data + " status: " + status);
        }));
    }).error(function(data, status, headers, config) {
        // file failed to upload
        console.log(data);
    });}
}]);

No i na sam koniec kontroler. W czasie inicjalizacji pobiera informacje z serwisu dotyczące użytkownika i przypisuje je do obiektu vm.profile.

Do tego funkcja vm.upload wykorzystująca ng-file-upload do komunikacji z WebAPI w celu zapisania w bazie danych informacji dotyczących zdjęcia profilowego.

Pierwsze podejście zakończone sukcesem

W końcu udało się doprowadzić wyświetlanie profili do tego punktu w którym wszystko działa, ale nie wygląda. Wyglądem (zarówno kodu, jak i strony) będę zajmował się w późniejszym czasie. Sporo się dzięki temu zadaniu nauczyłem. Przypomniałem sobie w jaki sposób faktycznie programuje się w Angularze, a także jak obsłużyć przesyłanie plików co na późniejszym etapie będzie niezwykle pomocne.

Pomożecie?

W związku z tym, że jest to moje pierwsze podejście do tego tematu to bardzo proszę bardziej doświadczonych Angularowo o drobną pomoc. Wejdźcie proszę na GitHuba i zróbcie mi mały Code Review. Co można zmienić/poprawić w tym co napisałem. Na pewno nie jest to zrobione super optymalnie i jest sporo miejsca na poprawki. Będę wdzięczny 🙂 A tym czasem do usłyszenia.

Related posts

  • Pingback: dotnetomaniak.pl()

  • Spoko wpis, może w wolnym czasie zagłębie się w Twoim projekcie i coś CI zasugeruję, ale to raczej nie tu, tylko jakoś się odezwę 😛 Co do kodu tak na szybko to radziłbym Ci wdrożyć Single Reposisibility Priciple, żeby Twoje metody na kontrolerze nie były takie grube. Potem będziesz miał piekło z refactorem, a i sporo kodu Ci się powieli. Pytanie odnośnie do bazy, celowo wydzieliłeś UsersExtensions do oddzielnej tabeli? Nie wolałeś dziedziczyć z Usera dostarczanego przez Identity? Ogólnie masz plusik bo mimo, że zaczynasz z angularem to kod wrzucasz, a nie piszesz, że „będzie jak kiedyś zrobie refactor” 😛 Będę wbijał częściej. Powodzenia 😉

    • Kajetan Duszyński

      Hej. Dzięki za sugestie 🙂 Oczywiście refactoring był w planach, ale skoro już teraz zwróciłeś na to uwagę to chyba od razu do tego przysiądę zanim się zakopię w kodzie 🙂

      Jeśli chodzi o UsersExtensions to przyznam się, że zbyt długo nad tym nie myślałem, ale może jeszcze nie jest za późno żeby to zmienić.

      Jeśli faktycznie miałbyś czas i ochotę spojrzeć na kod to zapraszam. Już jest na GitHubie i będę wdzięczny za wszelkie sugestie 🙂