연습 - @page 지시문을 사용하여 Blazor 앱에서의 탐색 변경

완료됨

Blazor에는 C# 코드가 앱의 URI를 관리하는 데 도움이 되는 탐색 상태 도우미가 있습니다. <a> 요소를 드롭인으로 대체하는 NavLink 구성 요소도 있습니다. NavLink의 기능 중 하나는 앱 메뉴에 대한 HTML 링크에 활성 클래스를 추가하는 것입니다.

팀에서 Blazing Pizza 앱을 시작했으며 피자 및 주문을 나타내는 Blazor 구성 요소를 빌드했습니다. 이제 앱에 체크 아웃 및 기타 주문 관련 페이지가 있어야 합니다.

이 연습에서는 새 체크 아웃 페이지를 추가하고, 앱에 위쪽 탐색을 추가한 다음, Blazor NavLink 구성 요소를 사용하여 코드를 개선합니다.

팀의 기존 앱 복제

참고

이 모듈에서는 로컬 개발에 .NET CLI(명령줄 인터페이스) 및 Visual Studio Code를 사용합니다. 이 모듈을 완료한 후 Visual Studio(Windows) 또는 Mac용 Visual Studio(macOS)를 사용하여 개념을 적용할 수 있습니다. 지속적인 개발의 경우 Windows, Linux 및 macOS에 Visual Studio Code를 사용합니다.

이 모듈에서는 .NET 6.0 SDK를 사용합니다. 기본 설정 터미널에서 다음 명령을 실행하여 .NET 6.0이 설치되어 있는지 확인합니다.

dotnet --list-sdks

그러면 다음과 같은 출력이 표시됩니다.

3.1.100 [C:\program files\dotnet\sdk]
5.0.100 [C:\program files\dotnet\sdk]
6.0.100 [C:\program files\dotnet\sdk]

6으로 시작하는 버전이 나열되어 있는지 확인합니다. 나열되는 버전이 없거나 명령을 찾을 수 없는 경우 최신 .NET 6.0 SDK를 설치합니다.

이전에 Blazor 앱을 만든 적이 없는 경우 Blazor 설치 지침에 따라 올바른 버전의 .NET을 설치하고 머신이 올바르게 설정되었는지 확인합니다. 앱 만들기 단계에서 중지합니다.

  1. Visual Studio Code를 엽니다.

  2. 보기를 선택하여 Visual Studio Code에서 통합 터미널을 엽니다. 그런 다음, 주 메뉴에서 터미널을 선택합니다.

  3. 터미널에서 프로젝트를 만들려는 위치로 이동합니다.

  4. GitHub에서 앱을 복제합니다.

    git clone https://github.com/MicrosoftDocs/mslearn-blazor-navigation.git BlazingPizza
    
  5. 파일을 선택한 다음, 폴더 열기를 선택합니다.

  6. 열기 대화 상자에서 BlazingPizza 폴더로 이동하고 폴더 선택을 선택합니다.

    Visual Studio Code에서 해결되지 않은 종속성에 대한 메시지가 표시될 수 있습니다. 복원을 선택합니다.

  7. 앱을 실행하여 모든 것이 제대로 작동하는지 확인합니다.

  8. Visual Studio Code에서 F5 키를 선택합니다. 또는 실행 메뉴에서 디버깅 시작을 선택합니다.

    Blazing Pizza 앱의 복제된 버전을 보여주는 스크린샷.

    일부 피자를 구성하고 주문에 추가합니다. 페이지 하단에서 주문 >을 선택합니다. 팀이 아직 체크 아웃 페이지를 만들지 않았기 때문에 기본 “404 찾을 수 없음” 메시지가 표시됩니다.

  9. Shift + F5를 선택하여 앱을 중지합니다.

체크 아웃 페이지 추가

  1. Visual Studio Code의 파일 탐색기에서 App.razor를 선택합니다.

    <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
        <Found Context="routeData">
            <RouteView RouteData="@routeData" />
        </Found>
        <NotFound>
            <LayoutView>
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
    

    <NotFound> 코드 블록은 고객이 존재하지 않는 페이지로 이동하려고 하면 표시되는 내용입니다.

  2. 파일 탐색기에서 페이지를 확장하고 폴더를 마우스 오른쪽 단추로 클릭하여 새 파일을 선택합니다.

  3. 새 파일의 이름을 Checkout.razor로 지정합니다. 이 파일에서 다음 코드를 작성합니다.

    @page "/checkout"
    @inject OrderState OrderState
    @inject HttpClient HttpClient
    @inject NavigationManager NavigationManager
    
    <div class="top-bar">
        <a class="logo" href="">
            <img src="img/logo.svg" />
        </a>
    
        <a href="" class="nav-tab">
            <img src="img/pizza-slice.svg" />
            <div>Get Pizza</div>
        </a>
    
    </div>
    
    <div class="main">
        <div class="checkout-cols">
            <div class="checkout-order-details">
                <h4>Review order</h4>
                @foreach (var pizza in Order.Pizzas)
                {
                    <p>
                        <strong>
                            @(pizza.Size)"
                            @pizza.Special.Name
                            (£@pizza.GetFormattedTotalPrice())
                        </strong>
                    </p>
                }
    
                <p>
                    <strong>
                        Total price:
                        £@Order.GetFormattedTotalPrice()
                    </strong>
                </p>
            </div>
        </div>
    
        <button class="checkout-button btn btn-warning">
            Place order
        </button>
    </div>
    
    @code {
        Order Order => OrderState.Order;
    }
    

    이 페이지는 현재 앱을 기반으로 하며 OrderState에 저장된 앱 상태를 사용합니다. 첫 번째 div는 앱의 새 머리글 탐색입니다. 인덱스 페이지에 추가해 보겠습니다.

  4. 파일 탐색기에서 페이지를 확장한 다음, index.razor를 선택합니다.

  5. <div class="main"> 클래스 위에 top-bar html을 추가합니다.

    <div class="top-bar">
        <a class="logo" href="">
            <img src="img/logo.svg" />
        </a>
    
        <a href="" class="nav-tab" >
            <img src="img/pizza-slice.svg" />
            <div>Get Pizza</div>
        </a>
    
    </div>
    

    이 페이지에 있는 경우 링크를 강조 표시하여 고객을 표시하는 것이 좋습니다. 팀은 이미 active css 클래스를 만들었으므로 nav-tab 스타일이 이미 포함된 class 특성에 active를 추가합니다.

    <div class="top-bar">
        <a class="logo" href="">
            <img src="img/logo.svg" />
        </a>
    
        <a href="" class="nav-tab active" >
            <img src="img/pizza-slice.svg" />
            <div>Get Pizza</div>
        </a>
    
    </div>
    
  6. Visual Studio Code에서 F5 키를 선택합니다. 또는 실행 메뉴에서 디버깅 시작을 선택합니다.

    이제 앱의 위쪽에 회사 로고를 포함한 멋진 메뉴 모음이 있습니다. 피자 몇 개를 추가하고 주문을 체크 아웃 페이지로 진행합니다. 피자가 나열되고 메뉴에 없는 활성 표시기가 표시됩니다.

    일부 피자가 있는 체크 아웃 페이지를 보여 주는 스크린샷

  7. Shift + F5를 선택하여 앱을 중지합니다.

고객이 주문을 제출하도록 허용

현재 체크 아웃 페이지에서는 고객이 주문을 제출할 수 없습니다. 앱의 논리는 주방으로 보낼 주문을 저장해야 합니다. 주문이 전송된 후 고객을 다시 홈페이지로 리디렉션해 보겠습니다.

  1. 파일 탐색기에서 페이지를 확장하고 Checkout.razor를 선택합니다.

  2. 단추 요소를 수정하여 PlaceOrder 메서드를 호출합니다. 다음과 같이 @onclick 특성 및 disabled 특성을 추가합니다.

    <button class="checkout-button btn btn-warning" @onclick="PlaceOrder" disabled=@isSubmitting>
      Place order
    </button>
    

    고객이 중복 주문을 하면 안 되므로 주문이 처리될 때까지 주문하기 단추를 비활성화합니다.

  3. @code 블록에서 Order Order => OrderState.Order; 코드 아래에 다음 코드를 추가합니다.

    bool isSubmitting;
    
    async Task PlaceOrder()
    {
        isSubmitting = true;
        var response = await HttpClient.PostAsJsonAsync(NavigationManager.BaseUri + "orders", OrderState.Order);
        var newOrderId= await response.Content.ReadFromJsonAsync<int>();
        OrderState.ResetOrder();
        NavigationManager.NavigateTo("/");
    }
    

    이전 코드는 주문하기 단추를 비활성화하고, pizza.db에 추가될 JSON을 게시하고, 주문을 지우고, NavigationManager를 사용하여 고객을 홈페이지로 리디렉션합니다.

    주문을 처리하는 코드를 추가해야 합니다. 이 작업에 대한 OrderController 클래스를 추가합니다. PizzaStoreContext.cs를 살펴보면 PizzaSpecials에 대한 엔터티 프레임워크 데이터베이스 지원만 있는 것을 알 수 있습니다. 먼저 이 문제를 해결해 보겠습니다.

주문 및 피자에 대한 엔터티 프레임워크 지원 추가

  1. 파일 탐색기에서 PizzaStoreContext.cs를 선택합니다.

  2. PizzaStoreContext 클래스를 다음 코드로 바꿉니다.

      public class PizzaStoreContext : DbContext
      {
            public PizzaStoreContext(
                DbContextOptions options) : base(options)
            {
            }
    
            public DbSet<Order> Orders { get; set; }
    
            public DbSet<Pizza> Pizzas { get; set; }
    
            public DbSet<PizzaSpecial> Specials { get; set; }
    
            public DbSet<Topping> Toppings { get; set; }
    
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                base.OnModelCreating(modelBuilder);
    
                // Configuring a many-to-many special -> topping relationship that is friendly for serialization
                modelBuilder.Entity<PizzaTopping>().HasKey(pst => new { pst.PizzaId, pst.ToppingId });
                modelBuilder.Entity<PizzaTopping>().HasOne<Pizza>().WithMany(ps => ps.Toppings);
                modelBuilder.Entity<PizzaTopping>().HasOne(pst => pst.Topping).WithMany();
            }
    
      }
    

    이 코드는 앱의 주문 및 피자 클래스에 대한 엔터티 프레임워크 지원을 추가합니다.

  3. Visual Studio Code의 메뉴에서 파일>새 텍스트 파일을 선택합니다.

  4. C# 언어를 선택하고 다음 코드를 입력합니다.

    using Microsoft.AspNetCore.Mvc;
    using Microsoft.EntityFrameworkCore;
    
    namespace BlazingPizza;
    
    [Route("orders")]
    [ApiController]
    public class OrdersController : Controller
    {
        private readonly PizzaStoreContext _db;
    
        public OrdersController(PizzaStoreContext db)
        {
            _db = db;
        }
    
        [HttpGet]
        public async Task<ActionResult<List<OrderWithStatus>>> GetOrders()
        {
            var orders = await _db.Orders
     	    .Include(o => o.Pizzas).ThenInclude(p => p.Special)
     	    .Include(o => o.Pizzas).ThenInclude(p => p.Toppings).ThenInclude(t => t.Topping)
     	    .OrderByDescending(o => o.CreatedTime)
     	    .ToListAsync();
    
            return orders.Select(o => OrderWithStatus.FromOrder(o)).ToList();
        }
    
        [HttpPost]
        public async Task<ActionResult<int>> PlaceOrder(Order order)
        {
            order.CreatedTime = DateTime.Now;
    
            // Enforce existence of Pizza.SpecialId and Topping.ToppingId
            // in the database - prevent the submitter from making up
            // new specials and toppings
            foreach (var pizza in order.Pizzas)
            {
                pizza.SpecialId = pizza.Special.Id;
                pizza.Special = null;
            }
    
            _db.Orders.Attach(order);
            await _db.SaveChangesAsync();
    
            return order.OrderId;
        }
    }
    

    이전 코드를 사용하면 앱이 현재 주문을 모두 받고 주문을 제출할 수 있습니다. [Route("orders")] Blazor 특성을 사용하면 이 클래스가 /orders/orders/{orderId}에 대한 들어오는 HTTP 요청을 처리할 수 있습니다.

  5. Ctrl+S를 사용하여 변경 내용을 저장합니다.

  6. 파일 이름에 OrderController.cs를사용합니다. 파일을 OrderState.cs와 동일한 디렉터리에 저장해야 합니다.

  7. 파일 탐색기에서 OrderState.cs를 선택합니다.

  8. 클래스 맨 밑의 RemoveConfiguredPizza 메서드 아래에 ResetOrder()를 수정하여 주문을 다시 설정합니다.

    public void ResetOrder()
    {
        Order = new Order();
    }
    

체크 아웃 기능 테스트

  1. Visual Studio Code에서 F5 키를 선택합니다. 또는 실행 메뉴에서 디버깅 시작을 선택합니다.

    앱이 컴파일되어야 하지만 주문을 만들고 체크 아웃을 시도하면 런타임 오류가 표시됩니다. 주문 및 피자에 대한 지원이 있기 전에 pizza.db SQLLite 데이터베이스가 만들어졌기 때문에 오류가 발생합니다. 새 데이터베이스를 올바르게 만들 수 있도록 파일을 삭제해야 합니다.

  2. Shift + F5를 선택하여 앱을 중지합니다.

  3. 파일 탐색기에서 pizza.db 파일을 삭제합니다.

  4. F5 키를 선택합니다. 또는 실행 메뉴에서 디버깅 시작을 선택합니다.

    테스트로 피자를 추가하고 체크 아웃으로 이동하여 주문을 제출합니다. 홈페이지로 리디렉션되고 주문이 비어 있는 것을 볼 수 있습니다.

  5. Shift + F5를 선택하여 앱을 중지합니다.

앱이 개선되고 있습니다. 피자 구성 및 체크 아웃이 있습니다. 고객이 주문을 제출한 후 피자 주문 상태를 볼 수 있도록 하려고 합니다.

주문 페이지 추가

  1. 파일 탐색기에서 페이지를 확장하고 폴더를 마우스 오른쪽 단추로 클릭하여 새 파일을 선택합니다.

  2. 새 파일의 이름을 MyOrders.razor로 지정합니다. 이 파일에서 다음 코드를 작성합니다.

    @page "/myorders"
    @inject HttpClient HttpClient
    @inject NavigationManager NavigationManager
    
    <div class="top-bar">
        <a class="logo" href="">
            <img src="img/logo.svg" />
        </a>
    
        <a href="" class="nav-tab">
            <img src="img/pizza-slice.svg" />
            <div>Get Pizza</div>
        </a>
    
        <a href="myorders" class="nav-tab active">
            <img src="img/bike.svg" />
            <div>My Orders</div>
        </a>
    </div>
    
    <div class="main">
        @if (ordersWithStatus == null)
        {
            <text>Loading...</text>
        }
        else if (!ordersWithStatus.Any())
        {
            <h2>No orders placed</h2>
            <a class="btn btn-success" href="">Order some pizza</a>
        }
        else
        {
            <div class="list-group orders-list">
                @foreach (var item in ordersWithStatus)
                {
                    <div class="list-group-item">
                        <div class="col">
                            <h5>@item.Order.CreatedTime.ToLongDateString()</h5>
                            Items:
                            <strong>@item.Order.Pizzas.Count()</strong>;
                            Total price:
                            <strong>£@item.Order.GetFormattedTotalPrice()</strong>
                        </div>
                        <div class="col">
                            Status: <strong>@item.StatusText</strong>
                        </div>
                        @if (@item.StatusText != "Delivered")
                        {
                            <div class="col flex-grow-0">
                                <a href="myorders/" class="btn btn-success">
                                    Track &gt;
                                </a>
                            </div>
                        }
                    </div>
                }
            </div>
        }
    </div>
    
    @code {
        List<OrderWithStatus> ordersWithStatus = new List<OrderWithStatus>();
    
        protected override async Task OnParametersSetAsync()
        {
          ordersWithStatus = await HttpClient.GetFromJsonAsync<List<OrderWithStatus>>(
              $"{NavigationManager.BaseUri}orders");
        }
    }
    

    현재의 모든 페이지에서 탐색을 변경하여 새 내 주문 페이지에 대한 링크를 포함해야 합니다. Checkout.razorIndex.razor를 열고 탐색을 다음 코드로 바꿉니다.

    <div class="top-bar">
        <a class="logo" href="">
            <img src="img/logo.svg" />
        </a>
    
        <a href="" class="nav-tab active" >
            <img src="img/pizza-slice.svg" />
            <div>Get Pizza</div>
        </a>
    
        <a href="myorders" class="nav-tab" >
            <img src="img/bike.svg" />
            <div>My orders</div>
        </a>
    
    </div>
    

    <a> 요소를 사용하면 active css 클래스를 추가하여 수동으로 활성 상태가 되는 페이지를 관리할 수 있습니다. NavLink 구성 요소를 대신 사용하도록 모든 탐색을 업데이트해 보겠습니다.

  3. 탐색이 있는 세 페이지(Index.razorCheckout.razor, MyOrders.razor) 모두 탐색 시 동일한 Blazor 코드를 사용합니다.

    <div class="top-bar">
        <a class="logo" href="">
            <img src="img/logo.svg" />
        </a>
    
        <NavLink href="" class="nav-tab" Match="NavLinkMatch.All">
            <img src="img/pizza-slice.svg" />
            <div>Get Pizza</div>
        </NavLink>
    
        <NavLink href="myorders" class="nav-tab">
            <img src="img/bike.svg" />
            <div>My Orders</div>
        </NavLink>
    </div>
    

    이제 active css 클래스가 NavLink 구성 요소에 의해 자동으로 페이지에 추가됩니다. 탐색이 있는 각 페이지에서 이 작업을 수행할 필요가 없습니다.

  4. 마지막 단계는 주문이 제출된 후 myorders 페이지로 리디렉션하도록 NavigationManager를 변경하는 것입니다. 파일 탐색기에서 페이지를 확장한 다음, Checkout.razor를 선택합니다.

  5. 다음과 같이 /myordersNavigationManager.NavigateTo()로 전달하여 PlaceOrder 메서드가 올바른 페이지로 리디렉션하도록 변경합니다.

    async Task PlaceOrder()
    {
        isSubmitting = true;
        var response = await HttpClient.PostAsJsonAsync($"{NavigationManager.BaseUri}orders", OrderState.Order);
        var newOrderId = await response.Content.ReadFromJsonAsync<int>();
        OrderState.ResetOrder();
        NavigationManager.NavigateTo("/myorders");
    } 
    
  6. Visual Studio Code에서 F5 키를 선택합니다. 또는 실행 메뉴에서 디버깅 시작을 선택합니다.

    주문 페이지를 보여주는 스크린샷.

    피자를 주문한 다음 데이터베이스에서 현재 주문을 볼 수 있어야 합니다.

  7. Shift + F5를 선택하여 앱을 중지합니다.