Stronicowanie danych w panelu zarządzania użytkownikami
Po długim weekendzie czas powrócić do konkursu Daj się poznać. Dziś chciałbym abyśmy zajęli się panelem zarządzania użytkownikami, który będzie widoczny jedynie dla userów w roli administratora. Podstawowe jego możliwości to :
- możliwość zablokowania/odblokowania użytkownika
- zmiana hasła użytkownikowi
- usunięcie użytkownika
Jak widać nie ma tu nic wymyślnego, ale też nie to miało być treścią projektu. Ponieważ jednak większość opcji jest zwykłą zmianą flagi użytkownika na bazie danych, a kod do CRUD-a i tak już widzieliście, postanowiłem, że skupimy się na stronicowaniu danych. Co to jest, i w jakim celu się to stosuje? Sama nazwa już sporo nam podpowiada. Istotą stronicowania jest podział danych, które mają zostać przedstawione użytkownikowi w GUI na mniejsze części (strony). Co dzięki temu zyskujemy? Po pierwsze znacznie zredukowany czas oczekiwania na wyświetlenie danych (abstrahując od faktu, że chęć pobrania np. 1 000 000 użytkowników na raz skończyłaby się najprawdopodobniej po kilkudziesięciu sekundach timeout-em). Po drugie nie jesteśmy skazani na scrollowanie w nieskończoność, aby odnaleźć kogoś z nazwiskiem rozpoczynającym się na literę Z. Głównie jednak, istota tkwi w wydajności. Kiedy wiemy, już co chcemy osiągnąć zabierzmy się do pracy. Pierwszym „elementem” będzie zaimplementowanie klasy oraz jej interfejsu, która będzie zawierać dane jednej strony oraz właściwość określającą ilość wszystkich stron:
public interface IPagedResult<TContent> { IEnumerable<TContent> Content { get; set; } int TotalPages { get; set; } } public class PagedResult<TContent> : IPagedResult<TContent> { public IEnumerable<TContent> Content { get; set; } public int TotalPages { get; set; } }
Jak widać zarówno klasa jak i interfejs są generyczne, aby stronicowanie innych danych w przyszłości nie wymagało implementacji dodatkowych klas. Przejdźmy zatem do API. Po pierwsze napiszemy metodę w serwisie domenowym, która na podstawie parametrów zwróci odpowiednią stronę o określonym rozmiarze:
public async Task<IPagedResult<UserReadModel>> GetUsersPageAsync(int pageNumber, int pageSize) { var qUsers = ReadRepository.NoTrackedQuery.Where(u => u.IsActive); var result = await qUsers.AsReadModel().Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync(); var usersNumber = await qUsers.CountAsync(u => u.IsActive); return new PagedResult<UserReadModel> { TotalPages = GetPagedResultTotalPages(usersNumber,pageSize), Content = result }; }
Krótki komentarz do kodu. Po pierwsze do zmiennej qUsers przypisane zostało nie śledzone IQueryable<UserEntity>, aby późniejsze linie były bardziej czytelne. Zmiennej result przypisana zostaje lista zawierająca obiekty ReadModel użytkowników. Pominięta zostaje pewna część rekordów w bazie danych zgodnie ze wzorem:
LP = (NS – 1) * RS
gdzie:
LP – liczba pominiętych rekordów
NS – numer strony
RS – rozmiar strony
W związku z tym, kiedy będziemy chcieli pobrać np. drugą stronę o rozmiarze 30, pominiemy:
(2 – 1) * 30 = 30 wierszy
Aby posiadać informację o łącznej liczbie stron musimy znać dwa parametry: łączną ilość użytkowników oraz rozmiar strony. Metoda GetPagedResultTotalPages zwraca tę informację korzystając ze wzoru:
LWS = ⌈LR /RS⌉
gdzie:
LWS – liczba wszystkich stron
LR – liczba rekordów
RS – rozmiar strony
Jeżeli ktoś z Was nie wie czym jest ten dziwny nawias, to jest to tzw. sufit (ang. ceiling). Jest to po prostu zaokrąglenie liczby w górę. Przykładowo sufit z 2.11 wynosi 3. Jeżeli zatem baza danych zawierałaby 100 użytkowników, to przy rozmiarze strony równym 30 potrzebowalibyśmy łącznie:
⌈100/30⌉ = 4 stron
Ostatnia rzecz związana z tą częścią kodu to implementacja metody AsReadModel, która „obcina” dane do niezbędnego minimum:
public static IQueryable<UserReadModel> AsReadModel(this IQueryable<UserEntity> that) { return that.Where(u => u.IsActive).Select(u => new UserReadModel { Id = u.Id, UserName = u.UserName, FirstName = u.FirstName, LastName = u.LastName, Email = u.Email, IsLocked = u.IsLocked }); }
Implementacja metody GetUsersPageAsync w warstwie Proxy oraz Web wygląda następująco:
//PROXY public async Task<IPagedResult<UserReadModel>> GetUsersPageAsync(int pageNumber, int pageSize) { using (var unitOfWork = _unitOfWorkFactory.Get()) { var userDomainService = _userDomainServiceFactory.Get(unitOfWork); return await userDomainService.GetUsersPageAsync(pageNumber, pageSize); } } //WEB [HttpGet("Users/{pageNumber}/Page/{pageSize}/Size")] public async Task<IPagedResult<UserReadModel>> GetUsersPageAsync(int pageNumber, int pageSize) { return await _userDomainServiceProxy.GetUsersPageAsync(pageNumber, pageSize); }
Raczej nie ma tu czego tłumaczyć 😛 Jedyna, niewidoczna zmiana, o której wspomniałem w tym wpisie to sposób działania UnitOfWork. Fabryka posiada teraz parametr, który określa czy na UOW otwarta zostanie transakcja bazodanowa:
Czas na stronę klienta. Tu też przedstawię jedynie niezbędne minimum. Tak prezentuje się ViewModel:
import userModels = require('../../user/models/user-models'); import services = require('../services/users-manage-service'); import data = require('../../../data'); import {inject} from 'aurelia-framework'; @inject(services.UsersManageService) export class UsersManageViewModel { usesManageService: services.IUsersManageService; users: userModels.UserModel[]; pageNumber = 1; pageSize = 5; totalPages = 1; constructor(usesManageService: services.UsersManageService) { this.usesManageService = usesManageService; } activate() { this.getUsers(); } getUsers() { this.usesManageService.getUsers(this.pageNumber, this.pageSize).then((result: data.IPagedResult<userModels.UserModel>) => { this.users = result.content; this.totalPages = result.totalPages; }); } //Inne metody getUsersPreviousPage() { this.pageNumber -= 1; if (this.pageNumber < 1) this.pageNumber = 1; this.getUsers(); } getUsersNextPage() { this.pageNumber += 1; if (this.pageNumber > this.totalPages) this.pageNumber = this.totalPages; this.getUsers(); } </pre> <pre>}
Jak widać domyślnie rozmiar strony wynosi 5. Metody getUsersPreviousPage oraz getUsersNextPage są zbindowane pod przyciski w widoku. Po ich kliknięciu i prostej walidacji wysłane zostaje żądanie na serwer z odpowiednimi parametrami. Nie wiem jak Wam, ale moim zdaniem pisanie takich metod nie jest najładniejsze. Lepiej byłoby wykorzystać jakiś mechanizm w Aureli, który umożliwiłby stałą obserwację wskazanej zmiennej i reagowanie na jej wszelkie zmiany. Tym zajmiemy się najprawdopodobniej w następnym wpisie konkursowym. No dobra, czas zobaczyć jak to działa:
Wygląda nie najgorzej, a co najważniejsze działa sprawnie. No to chyba wszystko na dziś. Przypominam, że cały kod projektu jest dla Was dostępny na githubie. Zachęcam również do śledzenia mnie na twitterze oraz facebooku 🙂
CYA !
Pingback: click here to play Bubble Shooter()
Pingback: Political Diyala()
Pingback: bet535 casino()
Pingback: engineering.uodiyala.edu.iq()
Pingback: Corporate Event Management Company Hyderabad()
Pingback: In Vitro DMPK()
Pingback: gvk bio company in india()
Pingback: wedding planners in hyderabad()
Pingback: Find more here()
Pingback: denver ppc management()
Pingback: bing ads guaranteed profit company()
Pingback: seo salt lake city()
Pingback: empresa informática()
Pingback: 家1()
Pingback: warehouse for sale()
Pingback: 검증사이트()
Pingback: satta matka, satta king, satta, matka, kalyan matka()
Pingback: free forex signals()