Tech for good

[Blazor & C# 핸즈온] 페이지, 라우팅 및 레이아웃을 사용하여 Blazor 탐색 개선 본문

IT/Computer Science

[Blazor & C# 핸즈온] 페이지, 라우팅 및 레이아웃을 사용하여 Blazor 탐색 개선

Diana Kang 2021. 12. 29. 15:14

https://docs.microsoft.com/ko-kr/learn/modules/use-pages-routing-layouts-control-blazor-navigation/

 

Use pages, routing, and layouts to improve Blazor navigation - Learn

Learn how to optimize your app's navigation, use parameters from the URL, and create reusable layouts in a Blazor web app.

docs.microsoft.com

 

위의 모듈을 공부하며 정리한 자료입니다.

 


 

* 이전 모듈은 아래 링크를 참고해주세요.

https://dianakang.tistory.com/61

 

[Blazor & C# 핸즈온] Blazor 웹 앱에서 데이터와 상호작용

https://docs.microsoft.com/ko-kr/learn/modules/interact-with-data-blazor-web-apps/ Interact with data in Blazor web apps - Learn Learn how to create a graphical user interface in a Blazor web app by..

dianakang.tistory.com

 

 

(Tip! 피자 앱을 만들기 위해서는

목차에 '[연습] - '이라고 되어있는 모듈만 따라하시면 됩니다.)

 

 


학습목표

  • 라우터 컴포넌트 및 NavLinks를 사용하여 Blazor 앱의 탐색 개선
  • 경로 매개변수(route parameter)로 기능 향상
  • Blazor 앱에서 레이아웃을 사용하여 중복 코드 줄이기

1. 소개

Blazor는 클라이언트 측 JavaScript 라이브러리를 관리하는 복잡성 없이 서버 측과 클라이언트 측 모두에서 애플리케이션 논리를 공유할 수 있는 .NET을 사용하여 대화형 웹 응용 프로그램을 만든다.

 

당신은 피자 배달 회사에 개발자로 고용되었으며, 피자 토핑을 커스터마이징할 수 있도록 피자를 표시하는 페이지를 이미 구축했다. 따라서 이제는 주문 페이지를 추가하고 애플리케이션의 네비게이션을 개선하려고 한다. 또한 고객이 원하는 것을 쉽게 찾을 수 있도록 애플리케이션 전체에 일관된 레이아웃을 만드려고 한다. 

 

이 모듈에서는 @page 지시문, Blazor 라우팅 및 NavLink 컴포넌트를 사용하여 애플리케이션을 통해 고객을 라우팅하는 방법에 대해 알아볼 것이다. 네비게이션이 작동하면 애플리케이션에 레이아웃을 추가하여 중복 코드를 줄일 수 있다.

 


(* 2단원은 생략합니다.)

3. 연습 - @page 지시문을 사용하여 Blazor 앱에서 네비게이션 변경하기

Blazor에는 C# 코드가 애플리케이션의 URI를 관리하는 데 도움을 주는 네비게이션 상태 도우미(A navigation state helper)가 있다. 또한 <a> 요소처럼 동작하는 NavLink 컴포넌트도 있다. NavLink의 기능 중 하나는 앱 메뉴의 HTML 링크에 active클래스를 추가하는 것이다. 

 

우리는 앞선 모듈에서 Blazing Pizza 앱을 시작하고 피자 및 주문을 나타내는 Blazor 컴포넌트를 구축하였다. 해당 모듈의 링크는 아래를 참조하면 된다. https://dianakang.tistory.com/61

 

[Blazor & C# 핸즈온] Blazor 웹 앱에서 데이터와 상호작용

https://docs.microsoft.com/ko-kr/learn/modules/interact-with-data-blazor-web-apps/ Interact with data in Blazor web apps - Learn Learn how to create a graphical user interface in a Blazor web app by..

dianakang.tistory.com

 

이제 앱에 결제 및 기타 주문 관련 페이지가 있어야 한다. 이번 연습에서는 새 결제 페이지를 추가하고 앱 상단에 네비게이션을 추가한 다음 Blazor NavLink 컴포넌트를 사용하여 코드를 개선한다.

 

 

3.1. 기존 애플리케이션 클론하기

앞선 모듈을 진행했다면 그때 작성해두었던 코드로 계속 진행하면 된다. 만약 그렇지 않다면 .NET 6.0을 설치한 후, 아래의 과정을 진행하여야 한다.

// dotnet sdk 버전 확인
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]

결과를 확인해보면 .NET 6.0이 설치되어있음을 알 수 있다. 만약 이전에 Blazor 앱을 만든 적이 없다면 해당 링크를 참고하여 올바른 버전의 .NET을 설치한 후 환경설정을 확인해보자. 

 

1) Visual Studio Code를 연다.

 

2) 보기(View)를 선택하여 Visual Studio Code에서 통합 터미널(integrated terminal)을 연 다음, 메인 메뉴에서 터미널(Terminal)을 선택한다. 

 

3) 터미널에서 프로젝트를 생성할 위치로 이동한다.

 

4) GitHub에서 앱을 클론한다.

git clone https://github.com/PhilStollery/blazing-pizza-for-learn-part-2.git BlazingPizza

 

5) 파일(File)을 선택한 다음, 폴더를 연다(Open folder)

 

6) 열기 대화상자에서 BlazingPizza 폴더로 이동하여 폴더를 선택한다(Select Folder).

(이때 Visual Studio Code는 해결되지 않은 종속성에 대해 묻는 메세지를 표시할 수 있다. 이때 복원(Restore)을 선택한다.)

 

7) 앱을 실행하여 모든 것이 올바르게 작동하는지 확인한다.

 

8) Visual Studio Code에서 F5 키를 누르거나 Run 메뉴에서 디버깅 시작(Start Debugging)을 선택한다. 

 

 

여기까지 하면 피자를 구성하고 주문에 추가하는 것까지 완료되었다. 즉, Order 버튼을 클릭하는 것까지는 구현이 된 것이다. 

 

 

9) Shift + F5를 눌러 앱 실행을 중지한다.

 

 

3.2. 결제 페이지 추가하기

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

// 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> 코드 블록을 통해 유저들이 앱을 실행하여 피자 주문을 시도했을 때 Sorry, there's nothing at this address. 라는 문구를 보게될 것이라는걸 알 수 있다. 아래의 그림처럼 말이다. 

 

 

2) Pages 폴더새로운 파일을 생성(New File)한다. 

 

3) 새 파일의 이름을 Checkout.razor로 지정하고, 다음 코드를 작성한다. 

// Pages/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" @onclick="PlaceOrder">
        Place order
    </button>
</div>

@code {
    Order Order => OrderState.Order;
    
    // 문서의 순서대로 실행할 때는 이 메서드를 추가하여 오류를 방지한다.
    // 그냥 PlaceOrder라는 빈 메서드 선언한 것이다.
    void PlaceOrder()
    {
    
    }
}

 

이 페이지는 현재 앱을 기반으로 하며, 앞서 선언했던 OrderState 클래스로 앱 상태를 활용한다.

 

 

4) Pages - index.razor을 선택한다. 

 

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

// Pages/index.razor

<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 클래스를 만들었으므로 이를 추가한다.

 

 

 

+ 참고!

 

active 클래스마우스를 클릭했을 때 적용

 

 

// Pages/index.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>

</div>

 

 

6) Visual Studio Code에서 F5 키를 누르거나 실행(Run) 메뉴에서 디버깅 시작(Start Debugging)을 선택한다. 

 

이제 앱 상단에 회사 로고를 포함한 멋진 메뉴 바가 있다. 피자를 추가한 후 결제 페이지에서 피자 결제를 진행하면, 나열된 피자들과 메뉴에서 누락된 활성 인디케이터를 확인할 수 있다. 

 

 

7) Shift + F5를 눌러 앱 실행을 중지한다.

 

 

3.3. 고객이 주문할 수 있도록 허용해주기

현재 결제 페이지에서는 고객이 주문을 할 수 없다. 애플리케이션의 로직은 피자를 주방으로 보내기 위한 주문을 저장해야 한다. 주문이 전송된 후, 고객을 홈페이지로 다시 리디렉션 해보겠다. 

 

1) 탐색기에서 Pages - Checkout.razor 파일을 선택한다. 

 

2) 버튼 요소를 PlaceOrder 메서드를 호출하는 코드로 바꾼다.

// Pages/Checkout.razor

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

 

고객이 중복 주문하는 것을 원하지 않으므로 주문이 처리될 때까지 주문하기 버튼을 비활성화한다. 

 

 

3) @code 블록의 Order Order => OrderState.Order; 아래에 이 코드를 추가한다.

// Pages/Checkout.razor

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("/");
}

 

코드를 자세히 살펴보면 아래와 같다. 참고로 아까 위에서 만들어두었던 빈 void 메서드는 지운다.

// Pages/Checkout.razor

@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("/");
    }
}

 

위의 코드는 Place order 버튼을 비활성화하고 pizza.db에 추가될 JSON을 포스트하며, 주문을 지우며 NavigationManager를 사용하여 고객을 홈페이지로 리디렉션할 수 있다.

 

 

  • NavigationManager

NavigationManager를 사용하여 C# 코드로 URI와 탐색을 관리할 수 있다. NavigationManager는 다음 표에 나와 있는 이벤트와 메서드를 제공한다.

출처: 이미지 클릭 시 이동

 


 

 

주문을 처리하기 위해서는 추가적인 코드가 필요하다. 이 작업을 위해 OrderController 클래스를 추가해보자!

PizzaStoreContext.cs를 보면, PizzaSpecials을 위해 엔터티 프레임워크 데이터베이스가 지원된다는 것을 알 수 있다.

 

 

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

1) 탐색기에서 PizzaStoreContext.cs를 선택한다.

 

2) PizzaStoreContext 클래스를 아래와 같이 바꾼다.

 

// PizzaStoreContext.cs

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; }

	// Specify DbSet properties etc
        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();
        }

  }

 

  • OnModelCreating 메서드 정의

: The DbContext class has a method called OnModelCreating that takes an instance of ModelBuilder as a parameter. This method is called by the framework when your context is first created to build the model and its mappings in memory.


 

전체 코드를 보면 아래와 같다.

 

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

 

 

 

3) Visual Studio Code의 메뉴에서 파일 - 새 파일을 선택한다.  

 

4) C# 언어를 선택하고 아래의 코드를 입력한다.

// OrderController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
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 속성은 이 클래스를 핸들링하여 들어오는 HTTP 요청(/orders, /orders/{orderId})을 처리할 수 있다.   

 

  • Routing 개념

Routing in MVC (출처: 이미지 클릭시 링크 이동)

 

domain/{controller}/{action}/{id}
// Program.cs

app.MapControllerRoute(
		name: "default",
		pattern: "{controller=Home}/{action=Index}/{id?}");
  • id는 선택사항이지만, controller와 action은 아님
  • 만약 controller와 action이 없는 경우는 그냥 default 설정으로 -!

 

5) Ctrl + S로 변경사항을 저장한다. 

 

6) 파일 이름을 OrderController.cs로 저장한다. 이 파일은 OrderState.cs 파일과 같은 위치에 있어야 한다.

 

7) 탐색기에서 OrderState.cs를 선택한다.

 

8) RemoveConfiguredPizza 메서드 아래에 해당 클래스를 추가하여 주문을 리셋한다. 

// OrderState.cs

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

 

// OrderState.cs 전체

using System.Collections.Generic;

namespace BlazingPizza
{
    public class OrderState
    {
        public bool ShowingConfigureDialog { get; private set; }
        public Pizza ConfiguringPizza { get; private set; }
        public Order Order { get; private set; } = new Order();

        public void ShowConfigurePizzaDialog(PizzaSpecial special)
        {
            ConfiguringPizza = new Pizza()
            {
                Special = special,
                SpecialId = special.Id,
                Size = Pizza.DefaultSize,
                Toppings = new List<PizzaTopping>(),
            };

            ShowingConfigureDialog = true;
        }

        public void CancelConfigurePizzaDialog()
        {
            ConfiguringPizza = null;

            ShowingConfigureDialog = false;
        }

        public void ConfirmConfigurePizzaDialog()
        {
            Order.Pizzas.Add(ConfiguringPizza);
            ConfiguringPizza = null;

            ShowingConfigureDialog = false;
        }

        public void RemoveConfiguredPizza(Pizza pizza)
        {
            Order.Pizzas.Remove(pizza);
        }

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

    }
}

 

 

3.5. 결제 기능 테스트하기

1) Visual Studio Code에서 F5를 누르거나 실행디버깅 시작을 선택한다. 

 

앱이 컴파일되어야 하지만 런타임 오류가 표시된다. 이것은 주문과 피자에 대한 지원이 있기 전에 pizza.db SQLLite 데이터베이스가 생성되었기 때문이다. 새 데이터베이스를 올바르게 만들 수 있도록 파일을 삭제할 필요가 있다.

 

2) Shift + F5를 눌러 앱 실행을 중지한다.

 

3) 탐색기에서 Pizza.db 파일을 삭제한다.

 

4) F5를 누르거나 실행 - 디버깅 시작을 선택한다. 

 

피자 추가하기, 계산하기, 주문하기를 테스트한다. 홈페이지로 리디렉션되어 주문이 비어있는 것을 확인할 수 있다. 

 

5) Shift + F5를 눌러 앱 실행을 중지한다. 

 

앱이 점점 개선되고 있다. 피자 구성과 계산하는 것까지 구현이 되었으니, 피자를 주문한 후 주문 상태까지 볼 수 있게 만들면 더 좋을 거 같다!

 

 

3.6. 주문 페이지 추가

1) 탐색기에서 Pages에 접근하여 새 파일 생성(New File)을 클릭한다.

 

2) 새 파일의 이름을 MyOrders.razor로 지정하고, 이 파일에 다음 코드를 작성한다.

// Pages/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");
    }
}

 

새로운 My orders 페이지에 링크를 포함하려면 현재 모든 페이지의 네비게이션을 변경해야 한다. Checkout.razor 파일과 Index.razor 파일을 열어 네비게이션을 아래와 같이 바꾼다.

 

// Checkout.razor, Index.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 클래스를 추가하여 수동으로 활성 페이지를 관리할 수도 있다. 하지만 NavLink 컴포넌트를 대신 사용하도록 모든 네비게이션을 업데이트하겠다.

 

 

3) 네비게이션이 있는 모든 페이지(Checkout.razor, Index.razor)에서 동일한 Blazor 코드를 사용한다.

// Checkout.razor, Index.razor

<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 클래스는 이제 NavLink 컴포넌트에 의해 페이지에 자동으로 추가된다. 네비게이션이 실행되는 각 페이지에서 이 작업을 수행하는 것을 기억할 필요는 없다.

 

 

4) 마지막 단계는 주문 후 myorders 페이지가 리디렉션 되도록 NavigationManager를 변경하는 것이다. 탐색기에서 Pages - Checkout.razor를 선택한다. 

 

 

5) 올바른 페이지로 리디렉션하기 위해 PlaceOrder 메서드를 변경한다.

// Pages/Checkout.razor

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");
}
// Pages/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>

    <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>

<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" @onclick="PlaceOrder" disabled=@isSubmitting>
        Place order
    </button>
</div>

@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("/myorders");
    }
}

 

 

6) Visual Studio Code에서 F5를 누르거나 실행 - 디버깅 시작을 선택한다. 

 

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

 

 

7) Shift + F5를 눌러 앱 실행을 중지한다. 

 

 


(* 4단원은 생략합니다.)

5. 연습 - 경로 매개변수를 사용하여 앱 탐색 개선

Blazor 경로 매개 변수를 사용하면 컴포넌트가 URL에 전달된 데이터에 액세스할 수 있다. 경로 매개변수(route parameter)는 OrderId로 나열된 주문에 앱이 접근할 수 있도록 만든다.

 

고객은 주문에 대한 자세한 정보를 보고싶어한다. 따라서 고객이 주문한 항목으로 바로 이동할 수 있도록 결제 페이지를 업데이트한다. 그 다음 현재 진행 중인 주문을 추적할 수 있도록 주문 페이지를 업데이트한다. 

 

이 연습에서는 경로 매개변수를 사용하는 새 주문 세부정보 페이지를 추가한다. 올바른 데이터 유형을 확인하기 위해 매개변수에 제약 조건을 추가하는 방법을 배울 수 있다.

 

 

5.1. 주문 세부정보 페이지 만들기

1) Visual Studio Code의 메뉴에서 파일 - 새 파일을 선택한다.

 

2) 언어로 C#을 선택한다.

 

3) 아래 코드를 사용하여 주문 세부정보 페이지 컴포넌트를 만든다. 

 

// Pages/OrderDetail.razor

@page "/myorders/{orderId}"
@inject NavigationManager NavigationManager
@inject HttpClient HttpClient

<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>

<div class="main">
    @if (invalidOrder)
    {
        <h2>Order not found</h2>
        <p>We're sorry but this order no longer exists.</p>
    }
    else if (orderWithStatus == null)
    {
        <div class="track-order">
            <div class="track-order-title">
                <h2>
                  <text>Loading...</text>
                </h2>
                <p class="ml-auto mb-0">
                    ...
                </p>
            </div>
        </div>
    }
    else
    {
        <div class="track-order">
            <div class="track-order-title">
                <h2>
                    Order placed @orderWithStatus.Order.CreatedTime.ToLongDateString()
                </h2>
                <p class="ml-auto mb-0">
                    Status: <strong>@orderWithStatus.StatusText</strong>
                </p>
            </div>
            <div class="track-order-body">
                <div class="track-order-details">
                  @foreach (var pizza in orderWithStatus.Order.Pizzas)
                  {
                      <p>
                          <strong>
                              @(pizza.Size)"
                              @pizza.Special.Name
                              (£@pizza.GetFormattedTotalPrice())
                          </strong>
                      </p>
                  }
                </div>
            </div>
        </div>
    }
</div>

@code {
    [Parameter] public int OrderId { get; set; }

    OrderWithStatus orderWithStatus;
    bool invalidOrder = false;

    protected override async Task OnParametersSetAsync()
    {
      try
      {
          orderWithStatus = await HttpClient.GetFromJsonAsync<OrderWithStatus>(
              $"{NavigationManager.BaseUri}orders/{OrderId}");
      }
      catch (Exception ex)
      {
          invalidOrder = true;
          Console.Error.WriteLine(ex);
      }
    }
}

 

이 페이지는 MyOrders 컴포넌트와 유사하다. 우리는 OrderController를 호출하고 있지만 이번에는 OrderId와 일치하는 특정 주문을 요청한다. 이 요청을 처리하는 코드를 추가해보겠다.

 

 

4) Ctrl + S를 눌러 변경 사항을 저장한다.

 

5) 파일 이름으로 OrderDetail.razor를 사용한다. 이 파일은 Pages 폴더에 위치해야 한다.

 

6) 탐색기에서 OrderController.cs를 선택한다. 

 

7) PlaceOrder 메서드 아래상태가 있는 주문을 반환하는 새 메서드를 추가한다.

 

// OrderController.cs

[HttpGet("{orderId}")]
public async Task<ActionResult<OrderWithStatus>> GetOrderWithStatus(int orderId)
{
    var order = await _db.Orders
        .Where(o => o.OrderId == orderId)
        .Include(o => o.Pizzas).ThenInclude(p => p.Special)
        .Include(o => o.Pizzas).ThenInclude(p => p.Toppings).ThenInclude(t => t.Topping)
        .SingleOrDefaultAsync();

    if (order == null)
    {
        return NotFound();
    }

    return OrderWithStatus.FromOrder(order);
}

 

// OrderController.cs 전체

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
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;
        }

        [HttpGet("{orderId}")]
        public async Task<ActionResult<OrderWithStatus>> GetOrderWithStatus(int orderId)
        {
            var order = await _db.Orders
            .Where(o => o.OrderId == orderId)
            .Include(o => o.Pizzas).ThenInclude(p => p.Special)
            .Include(o => o.Pizzas).ThenInclude(p => p.Toppings).ThenInclude(t => t.Topping)
            .SingleOrDefaultAsync();

            if (order == null)
            {
                return NotFound();
            }

            return OrderWithStatus.FromOrder(order);
        }
    }
}

 

이 코드를 통해 Order 컨트롤러는 URL에 OrderId가 있는 HTTP 요청에 응답할 수 있다. 그런 다음 메서드는 이 ID를 사용하여 데이터베이스를 쿼리하고 주문이 발견되면 OrderWithStatus 개체를 반환한다.

 

이제 고객이 결제할 때 이 새 페이지를 사용할 것이다. 그러기 위해서는 우선 Checkout.razor 컴포넌트를 업데이트해야 한다.  

 

 

8) 탐색기에서 Pages - Checkout.razor를 선택한다.

 

9) 완료된 주문의 OrderId를 사용하도록 NavigationManager.NavigateTo("/myorders")에 호출을 변경한다.

// Pages/Checkout.razor

NavigationManager.NavigateTo($"myorders/{newOrderId}");
// Pages/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>

    <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>

<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" @onclick="PlaceOrder" disabled=@isSubmitting>
        Place order
    </button>
</div>

@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($"myorders/{newOrderId}");
    }

}

 

 

기존 코드는 이미 완료된 주문으로부터 응답을 받아 newOrderId을 수집하고 있다. 이를 통해 해당 주문으로 직접 이동할 수 있다. 

 

 

10) Visual Studio Code에서 F5를 누르거나 실행 - 디버깅 시작을 선택한다.

이제 앱을 통해 주문을 하고 결제할 수 있다. 상세 주문 화면으로 이동하여 주문 상태를 확인한다. 

 

 

 

5.2. 경로 매개변수를 올바른 데이터 유형으로 제한

현재 앱은 http://localhost:5000/myorders/6 의 요청에 응답한다. 6을 다른 숫자 및 문자로 변경하여 직접 테스트할 수도 있다. 

 

 

1) 앱이 계속 실행 중이므로 http://localhost:5000/myorders/a로 이동한다.

 

이 오류는 OrderDetail 컴포넌트가 실행될 때, 정수로 선언된 컴포넌트 매개 변수인 OrderId에 경로 매개 변수를 전달하려고 하기 때문에 발생하는 것이다. 

 

이러한 종류의 예외는 앱에 대한 잠재적인 정보를 해커에게 제공할 수 있다. 따라서 우리는 이를 변경해야 한다.

 

 

2) Shift + F5를 눌러 앱 실행을 중지한다.

 

3) 탐색기에서 Pages - OrderDetail.razor를 선택한다.

 

4) 컴포넌트가 정수만 허용하도록 경로 매개변수를 변경한다.

// Pages/OrderDetail.razor

@page "/myorders/{orderId:int}"
// Pages/OrderDetail.razor 전체

@page "/myorders/{orderId:int}"	// 변경 부분
@inject NavigationManager NavigationManager
@inject HttpClient HttpClient

<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>

<div class="main">
    @if (invalidOrder)
    {
        <h2>Order not found</h2>
        <p>We're sorry but this order no longer exists.</p>
    }
    else if (orderWithStatus == null)
    {
        <div class="track-order">
            <div class="track-order-title">
                <h2>
                  <text>Loading...</text>
                </h2>
                <p class="ml-auto mb-0">
                    ...
                </p>
            </div>
        </div>
    }
    else
    {
        <div class="track-order">
            <div class="track-order-title">
                <h2>
                    Order placed @orderWithStatus.Order.CreatedTime.ToLongDateString()
                </h2>
                <p class="ml-auto mb-0">
                    Status: <strong>@orderWithStatus.StatusText</strong>
                </p>
            </div>
            <div class="track-order-body">
                <div class="track-order-details">
                  @foreach (var pizza in orderWithStatus.Order.Pizzas)
                  {
                      <p>
                          <strong>
                              @(pizza.Size)"
                              @pizza.Special.Name
                              (£@pizza.GetFormattedTotalPrice())
                          </strong>
                      </p>
                  }
                </div>
            </div>
        </div>
    }
</div>

@code {
    [Parameter] public int OrderId { get; set; }

    OrderWithStatus orderWithStatus;
    bool invalidOrder = false;

    protected override async Task OnParametersSetAsync()
    {
      try
      {
          orderWithStatus = await HttpClient.GetFromJsonAsync<OrderWithStatus>(
              $"{NavigationManager.BaseUri}orders/{OrderId}");
      }
      catch (Exception ex)
      {
          invalidOrder = true;
          Console.Error.WriteLine(ex);
      }
    }
}

 

5) 이제 누군가가 http://localhost:5000/myorders/non-number로 이동하려고 하면 Blazor 라우팅이 URL과 일치하는 항목을 찾지 못하고, 찾을 수 없는 페이지를 반환한다. 

 

6) Visual Studio Code 에서 F5를 누르거나 실행 - 디버깅 시작을 선택한다.

다른 주문 ID를 사용해보자. 유효한 주문이 아닌 정수를 사용하면 not found 메세지가 표시된다. 

정수가 아닌 주문 ID를 사용하는 경우 not found 페이지가 표시되며, 더 중요한 것은 앱에 처리되지 않은 예외가 없다는 것이다.

 

 

  • (유효한 주문의 숫자의 경우) http://localhost:5000/myorders/2

  • (유효하지 않은 주문의 숫자의 경우) http://localhost:5000/myorders/6 

 

 

7) Shift + F5를 눌러 앱 실행을 중지한다.

 

 

 

5.3. 주문 페이지 업데이트

현재 MyOrders 페이지에 자세한 내용을 볼 수 있는 링크가 있지만, URL이 잘못되었다.

 

1) 탐색기에서 Pages - MyOrders.razor를 선택한다.

 

2) <a> 요소를 아래와 같이 바꾼다.

//Pages/MyOrder.razor

<a href="myorders/@item.Order.OrderId" class="btn btn-success">
// Pages/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/@item.Order.OrderId" 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");
    }
}

 

이 연습에 대한 마지막 피자 주문을 하여 이 작업을 테스트할 수 있다. 그런 다음 My Orders를 선택하고 Track > link를 따른다.

 

 

 

(* 6단원은 생략합니다.)

7. 연습 - 코드의 중복을 줄이기 위해 Blazor 레이아웃 추가

Blazing Pizza 앱에 페이지를 추가하면 네비게이션 HTML을 복사하고 있음을 알 수 있다. Blazor는 Blazor 레이아웃이라는 한 곳에서 이러한 종류의 페이지 스케폴딩을 만들 수 있도록 지원한다.

 

이제 여러 페이지에 복제된 HTML이 많이 있다. 다음으로 네비게이션 및 회사 정보를 한 곳에서 추가할 수 있도록 전체 앱에 대한 레이아웃을 만든다.

 

이 연습에서는 MainLayout 컴포넌트를 만든다. 특정 페이지에 이 레이아웃을 사용한 다음 전체 앱의 기본 레이아웃으로 만드는 방법을 볼 수 있다. 

 

 

7.1. MainLayout 컴포넌트 추가

1) 탐색기에서 Shared - MainLayout.razor 파일을 연다.

 

(만약 해당 파일이 없는 경우에는 Visual Studio Code의 메뉴에서 파일 - 새 파일을 선택하고, 언어로 C#을 선택하여 Shared 폴더MainLayout.razor 파일을 생성한다.)

 

 

2) 레이아웃 컴포넌트를 만들고 페이지에서 네비게이션 HTML을 복사한다.

// Shared/MainLayout.razor

@inherits LayoutComponentBase

<div id="app">

  <header 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>
  </header>

  <div class="content">
    @Body
  </div>

  <footer>
    &copy; Blazing Pizza @DateTime.UtcNow.Year
  </footer>

</div>

 

이 레이아웃은 _Host.cshtml의 일부 마크업을 사용했으므로 해당 파일로 가서 이를 삭제해야 한다.

 

 

3) Ctrl + S로 변경된 사항을 저장한다.

 

4) 탐색기에서 Pages - _Host.cshtml을 선택한다.

 

5) Blazor 앱 컴포넌트를 둘러싼 요소를 아래와 같이 변경한다.

 

//Pages/_Host.cshtml

<div id="app">
    <div class="content">
        <component type="typeof(App)" render-mode="ServerPrerendered" />
    </div>
</div>

기존의 위 코드를 아래의 코드로 변경한다.

 

//Pages/_Host.cshtml

<component type="typeof(App)" render-mode="ServerPrerendered" />

 

전체 코드를 보면 아래와 같다.

Shared/MainLayout.razor
Pages/_Host.cshtml

 

7.2. 페이지 컴포넌트에서 Blazor 레이아웃 사용

1) 탐색기에서 Pages - Index.razor를 선택한다.

 

2) top-bar <div> 요소를 삭제하고 @page 지시문 아래에 새 레이아웃에 대한 참조를 추가한다.

// Pages/Index.razor

@layout MainLayout

 

 

// 변경 전 Pages/Index.razor

@page "/"
@inject HttpClient HttpClient
@inject NavigationManager NavigationManager
@inject OrderState OrderState
@using System.Net.Http.Json

<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>


<div class="main">
  <h1>Blazing Pizzas</h1>
  <ul class="pizza-cards">
    @if (specials != null)
    {
      @foreach (var special in specials)
      {
        <li @onclick="@(() => OrderState.ShowConfigurePizzaDialog(special))" style="background-image: url('@special.ImageUrl')">
          <div class="pizza-info">
            <span class="title">@special.Name</span>
            @special.Description
            <span class="price">@special.GetFormattedBasePrice()</span>
          </div>
        </li>
      }
    }
  </ul>
</div>


<div class="sidebar">
    @if (order.Pizzas.Any())
    {
        <div class="order-contents">
            <h2>Your order</h2>

            @foreach (var configuredPizza in order.Pizzas)
            {
              <div class="cart-item">
                  <div class="title">@(configuredPizza.Size)" @configuredPizza.Special.Name</div>
                  <div class="item-price">
                      @configuredPizza.GetFormattedTotalPrice()
                  </div>
                  <a @onclick="@(() => OrderState.RemoveConfiguredPizza(configuredPizza))" class="delete-item">x</a>
              </div>
            }
        </div>
    }
    else
    {
        <div class="empty-cart">Choose a pizza<br>to get started</div>
    }

    <div class="order-total @(order.Pizzas.Any() ? "" : "hidden")">
        Total:
        <span class="total-price">@order.GetFormattedTotalPrice()</span>
        <a href="checkout" class="@(OrderState.Order.Pizzas.Count == 0 ? "btn btn-warning disabled" : "btn btn-warning")">
            Order >
        </a>
    </div>
</div>


@if (OrderState.ShowingConfigureDialog)
{
  <ConfigurePizzaDialog
      Pizza="OrderState.ConfiguringPizza"
      OnCancel="OrderState.CancelConfigurePizzaDialog"
      OnConfirm="OrderState.ConfirmConfigurePizzaDialog" />
}


@code {
  Order order => OrderState.Order;
  List<PizzaSpecial> specials = new List<PizzaSpecial>();

  protected override async Task OnInitializedAsync()
  {
      specials = await HttpClient.GetFromJsonAsync<List<PizzaSpecial>>(NavigationManager.BaseUri + "specials");
  }
}

 

3) 레이아웃 참조가 상단에 존재하므로 기존의 Blazing Pizzas 요소는 삭제해도 된다.  그 결과, 위의 코드가 아래의 코드처럼 변경된다.

 

// 변경 후 Pages/Index.razor

@page "/"
@inject HttpClient HttpClient
@inject NavigationManager NavigationManager
@inject OrderState OrderState
@using System.Net.Http.Json

@layout MainLayout


<div class="main">
  <h1>Blazing Pizzas</h1>
  <ul class="pizza-cards">
    @if (specials != null)
    {
      @foreach (var special in specials)
      {
        <li @onclick="@(() => OrderState.ShowConfigurePizzaDialog(special))" style="background-image: url('@special.ImageUrl')">
          <div class="pizza-info">
            <span class="title">@special.Name</span>
            @special.Description
            <span class="price">@special.GetFormattedBasePrice()</span>
          </div>
        </li>
      }
    }
  </ul>
</div>


<div class="sidebar">
    @if (order.Pizzas.Any())
    {
        <div class="order-contents">
            <h2>Your order</h2>

            @foreach (var configuredPizza in order.Pizzas)
            {
              <div class="cart-item">
                  <div class="title">@(configuredPizza.Size)" @configuredPizza.Special.Name</div>
                  <div class="item-price">
                      @configuredPizza.GetFormattedTotalPrice()
                  </div>
                  <a @onclick="@(() => OrderState.RemoveConfiguredPizza(configuredPizza))" class="delete-item">x</a>
              </div>
            }
        </div>
    }
    else
    {
        <div class="empty-cart">Choose a pizza<br>to get started</div>
    }

    <div class="order-total @(order.Pizzas.Any() ? "" : "hidden")">
        Total:
        <span class="total-price">@order.GetFormattedTotalPrice()</span>
        <a href="checkout" class="@(OrderState.Order.Pizzas.Count == 0 ? "btn btn-warning disabled" : "btn btn-warning")">
            Order >
        </a>
    </div>
</div>


@if (OrderState.ShowingConfigureDialog)
{
  <ConfigurePizzaDialog
      Pizza="OrderState.ConfiguringPizza"
      OnCancel="OrderState.CancelConfigurePizzaDialog"
      OnConfirm="OrderState.ConfirmConfigurePizzaDialog" />
}


@code {
  Order order => OrderState.Order;
  List<PizzaSpecial> specials = new List<PizzaSpecial>();

  protected override async Task OnInitializedAsync()
  {
      specials = await HttpClient.GetFromJsonAsync<List<PizzaSpecial>>(NavigationManager.BaseUri + "specials");
  }
}

 

 

4) Visual Studio Code에서 F5를 누르거나 실행 - 디버깅 시작을 선택한다. 

 

새 홈페이지에는 자동으로 업데이트 되는 저작권 바닥글(copyright footer)가 있다. 이 레이아웃을 사용하지 않는 페이지를 보려면 My Orders 페이지를 클릭하거나 http://localhost:5000/help 와 같은 잘못된 페이지로 이동하면 된다. 그러면 404 Page not found 라는 문구 대신 아래와 같은 문구를 확인할 수 있을 것이다.

 

5) Shift + F5를 눌러 앱 실행을 중지한다.

 

 

7.3. 새 레이아웃을 사용하도록 모든 페이지 업데이트 하기

이제 앱의 모든 페이지에 @layout MainLayout 지시문을 추가할 수 있다. Blazor에는 더 나은 솔루션이 있기 때문에 먼저 Index.razor에 추가된 레이아웃 지시문을 삭제한다.

 

1) Index.razor의 컴포넌트인 @layout MainLayout을 삭제한다.

2) 탐색기에서 Pages - MyOrders.razor를 선택한다.

3) top-bar div 요소를 삭제한다.

4) 탐색기에서 Pages - OrderDetail.razor를 선택한다. 

5) top-bar  div 요소를 삭제한다.

6) 탐색기에서 Pages - Checkout.razor를 선택한다.

7) top-bar  div 요소를 삭제한다.

 

 

App.razor 컴포넌트는 페이지가 라우팅되는 방식을 변경할 수도 있지만, Blazor에 기본 레이아웃을 사용하도록 지시할 수도 있다. 

 

1) 탐색기에서 App.razor를 선택한다.

2) RouteView 요소에 DefaultLayout="typeof(MainLayout)"선언을 추가한다.

3) Layout="typeof(MainLayout)"을 LayoutView에 추가한다. 

4) 그럼 이제 App.razor가 다음과 같이 보일 것이다.

 

// 변경 전 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>

 

// 변경 후 App.razor

<Router AppAssembly="typeof(Program).Assembly" Context="routeData">
    <Found>
        <RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="typeof(MainLayout)">
            <div class="main">Sorry, there's nothing at this address.</div>
        </LayoutView>
    </NotFound>
</Router>

 

 

 

7.4. 새 레이아웃 테스트 하기

1) Visual Studio Code에서 F5를 누르거나 실행 - 디버깅 시작을 선택한다.

기본 레이아웃을 사용하는 것의 장점은 모든 페이지에 적용할 수 있으며 not found 페이지를 LayoutView로 사용할 수 있다는 것이다. 즉, 이제 모든 페이지에서 푸터를 확인할 수 있게 된다.

 

 

2) Shift + F5를 눌러 앱 실행을 중지한다.

 

 

앱을 위해 수행되어야 하는 작업은 이 모듈에서 완료가 되었다. 다음 모듈에서는 양식 및 유효성 검사를 처리하기 위한 방법을 살펴볼 것이다.


8. 요약

이제 Pizza 앱은 네비게이션 기능이 향상되었으며, 레이아웃을 사용하여 전체 앱에서 공통 페이지 배열을 공유함으로써 앱을 단순화할 수 있게 되었다. 이번 모듈에서 우리는 페이지 매개 변수와 Blazor 컴포넌트 간에 데이터를 공유하여 앱의 기능을 개선하는 방법에 대해 살펴보았다.  

 

Blazor를 사용하면 C# 경험을 재사용하여 프론트엔드 웹 개발에 적용할 수 있다. 이번 모듈에서 배운 ASP.NET Core Blazor 개념을 되짚어보며 이번 포스팅을 마무리 짓겠다 :)

 

 

Introduction to ASP.NET Core Blazor

Explore ASP.NET Core Blazor, a way to build interactive client-side web UI with .NET in an ASP.NET Core app.

docs.microsoft.com

 

ASP.NET Core Blazor routing and navigation

Learn how to manage request routing in Blazor apps and how to use the Navigation Manager and NavLink component for navigation.

docs.microsoft.com

 

ASP.NET Core Blazor layouts

Learn how to create reusable layout components for Blazor apps.

docs.microsoft.com