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:

 

UOW

 

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:

result

 

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 !

You may also like...