演習 - @page ディレクティブを使用して Blazor アプリのナビゲーションを変更する

完了

Blazor には、C# コードでアプリの URI を管理するのに役立つナビゲーション状態ヘルパーが用意されています。 <a>要素のドロップイン置換である NavLink コンポーネントもあります。 NavLink の機能の 1 つは、アプリのメニューのために、HTML リンクにアクティブなクラスを追加することです。

チームは Blazing Pizza アプリに着手して、ピザと注文を表す Blazor コンポーネントを構築しました。 ここで、アプリではチェックアウトやその他の注文関連ページを追加する必要があります。

この演習では、新しいチェックアウト ページを追加し、アプリに上部のナビゲーションを追加した後、Blazor NavLink コンポーネントを使用してコードを改善します。

チームの既存の Web アプリを複製する

このモジュールでは、ローカル開発に .NET コマンド ライン インターフェイス (CLI) と Visual Studio Code を使用します。 このモジュールの完了後、Visual Studio (Windows) または Visual Studio for Mac (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 アプリの複製バージョンを示すスクリーンショット。

    ピザをいくつか構成し、注文に追加します。 ページの下部にある [ order > ] を選択します。 チェックアウト ページがまだないため、既定の "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 クラスを作成しているので、active スタイルが既に含まれている class 属性に nav-tab を追加します。

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

    上記のコードでは、[ 注文 ] ボタンを無効にし、JSON を投稿して pizza.dbに追加し、注文をクリアし、 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 キーを押します。 または、[ 実行 ] メニューの [ デバッグの開始] を選択します。

    アプリはコンパイルする必要がありますが、注文を作成してチェックアウトしようとすると、実行時エラーが表示されます。 このエラーは、注文とピザがサポートされる前に sqlLite データベースpizza.dbが作成されたために発生します。 新しいデータベースが正しく作成されるように、このファイルを削除する必要があります。

  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.razor、Checkout.razorMyOrders.razor) を含む 3 つのページすべてで、ナビゲーションに同じ 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. 最後の手順は、注文が行われた後に NavigationManager ページへのリダイレクトが行われるように myorders を変更することです。 エクスプローラーで [ ページ] を展開し、[ Checkout.razor] を選択します。

  5. PlaceOrder/myorders に渡して、正しいページにリダイレクトされるように NavigationManager.NavigateTo() メソッドを変更します。

    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 を選択してアプリを停止します。