{"id":19702157,"url":"https://github.com/dipsas/blazorworkshop","last_synced_at":"2026-04-15T00:32:18.105Z","repository":{"id":90879766,"uuid":"203757297","full_name":"DIPSAS/BlazorWorkshop","owner":"DIPSAS","description":null,"archived":false,"fork":false,"pushed_at":"2019-09-22T09:57:37.000Z","size":5319,"stargazers_count":3,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-02-27T16:18:38.101Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"CSS","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DIPSAS.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-08-22T09:08:39.000Z","updated_at":"2019-09-22T09:57:40.000Z","dependencies_parsed_at":null,"dependency_job_id":"494bc6f3-7b42-4da7-bf12-0dfceebeb90d","html_url":"https://github.com/DIPSAS/BlazorWorkshop","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/DIPSAS/BlazorWorkshop","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DIPSAS%2FBlazorWorkshop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DIPSAS%2FBlazorWorkshop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DIPSAS%2FBlazorWorkshop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DIPSAS%2FBlazorWorkshop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DIPSAS","download_url":"https://codeload.github.com/DIPSAS/BlazorWorkshop/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DIPSAS%2FBlazorWorkshop/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31821510,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-14T18:05:02.291Z","status":"ssl_error","status_checked_at":"2026-04-14T18:05:01.765Z","response_time":153,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-11T21:13:41.219Z","updated_at":"2026-04-15T00:32:18.097Z","avatar_url":"https://github.com/DIPSAS.png","language":"CSS","readme":"# BlazorWorkshop\n\n## Before you start\n\nEnsure you have the same .NET version installed as this workshop is developed with. This workshop compiles and runs using .NET Core 3.0.0 preview 9 or newer. Download and install both the SDK and the runtime from [.NET Core's downloads page](https://dotnet.microsoft.com/download/dotnet-core/3.0).\n\n### Working from Visual Studio Code\n\nIf you're using [Visual Studio Code](https://code.visualstudio.com/) as an editor, it is recommended to use the [C#](https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp) and [Razor+](https://marketplace.visualstudio.com/items?itemName=austincummings.razor-plus) extensions.\n\n### Working from Visual Studio\n\nIf you're using Visual Studio you need enable the preview by going into Tools -\u003e Options -\u003e Environment -\u003e Preview Features and checking the appropriate boxes. Then you can simply open the Workshop solution file in the project root.\n\n## Getting started\n\nIn this workshop we will create an online store for ordering pharmaceuticals and having them delivered to us, analogous to food delivery services such as Wolt and Foodora.\n\nAs we all know, naming things is one of the hardest problems in computer science. That's why we've come up with a few suggestions for what you can name your online drug store (with a slight mix of Norwegian and English names, in case you want to go international):\n\n- Woltarol\n- Paracet Express\n- Paralgin Fort\n- Uber Drugs\n- TakeAway the Pain\n- Pillivery\n- Dependency Injection Service\n- Adams dopkasse\n- Mobile Dispensary\n- Pharmacuties\n\nThis workshop is split into four parts of approximately twenty steps each, let's start Blazoring it! If you get lost along the way or simply want to see the finished product, checkout to the `stage2`, `stage3` or `final` branches to see the next steps or the completed solution.\n\n## Project structure\n\nYou will find several projects in this repository.\n\n- **Workshop.Client** contains all UI components for the application. It is compiled as a .NET Standard project because it will be running on the Mono runtime in the browser.\n- **Workshop.Server** hosts the Blazor app and the backend services. It will serve the runtime and the client application to the browser.\n- **Workshop.Shared** contains shared model types.\n- **Workshop.ComponentsLibrary** contains helper code that will be used later.\n\n## Stage 1\n\n### Adding some drugs\n\nEnsure that the server starts and works properly by building and running it. You may need to restore the projects packages before you can launch the server. Any IDE will do this for you, but you can do it manually by running `dotnet restore` in the project root folder.\n\nIf you're using Visual Studio (Code) you can press F5, otherwise you can run the server from your terminal with `dotnet run --project Workshop.Server`.\n\nVisiting [`http://localhost:5000`](http://localhost:5000) should take you to an empty page with the title \"Insert Business Name Here\".\n\nOpen [`Pages/Index.razor`](./Workshop.Client/Pages/Index.razor) in the `Client` project to see the code for the home page. Feel free to change the title and restart the server.\n\nNext, add a `@code` block to the file and initialize a list field to keep track of your deals:\n\n```csharp\n@code {\n    List\u003cDrugDeal\u003e deals;\n}\n```\n\nThe `DrugDeal` type is already defined for you in the `Shared` project.\n\nTo fetch the available list of deals we need to call an API on the backend. Blazor provides a preconfigured `HttpClient` through dependency injection that is already set up with the correct base address. You can use the `@inject` directive to inject an `HttpClient` into the component using dependency injection:\n\n```csharp\n@page \"/\"\n@inject HttpClient HttpClient\n```\n\nOverride the lifecycle hook `OnInitializedAsync` and use the `GetJsonAsync\u003cT\u003e()` method to get the deals and automatically deserialize the JSON into the `DrugDeal` type.\n\n```csharp\n@code {\n  List\u003cDrugDeal\u003e deals;\n\n  protected async override Task OnInitializedAsync()\n  {\n    deals = await HttpClient.GetJsonAsync\u003cList\u003cDrugDeal\u003e\u003e(\"deals\");\n  }\n}\n```\n\nTo list the deals on your page we need to add some markup. Replace the title from earlier with the following:\n\n```html\n\u003cdiv class=\"main\"\u003e\n  \u003cul class=\"drug-cards\"\u003e\n    @if (deals != null) { @foreach (var deal in deals) {\n    \u003cli style=\"background-image: url('@deal.ImageUrl')\"\u003e\n      \u003cdiv class=\"drug-info\"\u003e\n        \u003cspan class=\"title\"\u003e@deal.Name\u003c/span\u003e\n        @deal.Description\n        \u003cspan class=\"price\"\u003e@deal.GetFormattedBasePrice()\u003c/span\u003e\n      \u003c/div\u003e\n    \u003c/li\u003e\n    } }\n  \u003c/ul\u003e\n\u003c/div\u003e\n```\n\nIf you build and run the application again you should see some a list of drugs along with their descriptions and prices. Feel free to add new drugs in [`SeedData.cs`](./Workshop.Server/SeedData.cs), but you need to drop the database to force it to re-seed. New images can be added to the `Client` projects `wwwroot/img` directory.\n\n### Adding a navigation bar\n\nWe would like to add some structure to the layout of our web application, and we'll start with a navigation bar. In the `Client` project, navigate to [`Shared/MainLayout.razor`](./Workshop.Client/Shared/MainLayout.razor). This is where the main layout for the web application is defined. Currently the only thing it contains is the body which you have been working on up until now. To add a header with a branding logo and navigation, replace its content with the following code:\n\n```html\n@inherits LayoutComponentBase\n\n\u003cdiv class=\"top-bar\"\u003e\n  \u003cimg class=\"logo\" src=\"img/logo.png\" /\u003e\n\n  \u003cNavLink href=\"\" class=\"nav-tab\" Match=\"NavLinkMatch.All\"\u003e\n    \u003cimg src=\"img/medicine.svg\" /\u003e\n    \u003cdiv\u003eGet Drugs\u003c/div\u003e\n  \u003c/NavLink\u003e\n\u003c/div\u003e\n\n\u003cdiv class=\"content\"\u003e\n  @Body\n\u003c/div\u003e\n```\n\nBuild and run the app to see your web store taking shape.\n\n## Stage 2\n\n### Adding a drug customization dialog\n\nIn this section we will look at enabling the users to customize their deal before adding it to their order.\n\nWe want a customization dialog to pop up when a user clicks a drug deal. In Blazor you can attach C# delegates to events where you traditionally would add javascript.\nIn the `Client` projects' [`Pages/Index.razor`](./Workshop.Client/Pages/Index.razor), add the following `@onclick` handler to your existing markup:\n\n```html\n@foreach (var deal in deals) {\n\u003cli\n  @onclick=\"@(() =\u003e Console.WriteLine(deal.Name))\"\n  style=\"background-image: url('@deal.ImageUrl')\"\n\u003e\n  \u003cdiv class=\"drug-info\"\u003e\n    \u003cspan class=\"title\"\u003e@deal.Name\u003c/span\u003e\n    @deal.Description\n    \u003cspan class=\"price\"\u003e@deal.GetFormattedBasePrice()\u003c/span\u003e\n  \u003c/div\u003e\n\u003c/li\u003e\n}\n```\n\nIn the `@onclick` attribute you see we added an [expression lambda](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions#expression-lambdas) which prints the deal name to the browser console when the user clicks a deal.\nThe `@` symbol is used in Razor files to indicate the start of C# code and parens are used to show where the code begins and ends.\n\nRun the application and check the browser developer console for print statements when clicking drug deals.\n\nPrinting the name to the console is all well and good, but we want to create a custom handler that shows a dialog for customizing the drug deal.\nFirst, add some additional fields next to the list of deals. One of them will hold the `Drug` we are currently configuring and the other will determine whether or not to show the configure dialog.\n\n```csharp\nDrug configuringDrug;\nbool showingConfigureDialog;\n```\n\nNext, add a `ShowConfigureDrugDialog` method to the `@code` block in `Index.razor` for handling when a drug deal is clicked.\n\n```csharp\nvoid ShowConfigureDrugDialog(DrugDeal deal)\n{\n  configuringDrug = new Drug()\n  {\n    Deal = deal,\n    DealId = deal.Id,\n    Quantity = Drug.DefaultQuantity,\n  };\n\n  showingConfigureDialog = true;\n}\n\n```\n\nWe also need to update the `@onclick` handler to call our new method instead of printing to the console.\n\n```html\n\u003cli\n  @onclick=\"@(() =\u003e ShowConfigureDrugDialog(deal))\"\n  style=\"background-image: url('@deal.ImageUrl')\"\n\u003e\u003c/li\u003e\n```\n\nIf you run the application now, nothing will happen including printing to the console. This is because we need to add a customization dialog.\nTo achieve this we need to create a new component. We have already provided you with a `ConfigureDrugDialog` component found under the `Shared` folder in the `Client` project.\nThe dialog component needs a `Drug` parameter that specifies the drug being configured and two callbacks `OnCancel` and `OnConfirm`.\n\nUpdate [`Pages/Index.razor`](./Workshop.Client/Pages/Index.razor) to show the `ConfigureDrugDialog` when a drug deal has been selected. The dialog is styled to overlay the page so it does not matter where you put the code block.\nThe code block will show a `ConfigureDrugDialog` component on the page if the `showingConfigureDialog` is set to true, and it will send the selected drug to the component\n\n```html\n@if (showingConfigureDialog) {\n\u003cConfigureDrugDialog\n  Drug=\"configuringDrug\"\n  OnCancel=\"CancelConfigureDrugDialog\"\n  OnConfirm=\"ConfirmDrugDialog\"\n/\u003e\n}\n```\n\nIn [`Pages/Index.razor`](./Workshop.Client/Pages/Index.razor), we also need to implement the `CancelConfigureDrugDialog` and `ConfirmDrugDialog` methods that we sent as parameters to the `ConfigureDrugDialog` component. We will leave the `ConfirmDrugDialog` empty for now.\n\n```csharp\nvoid CancelConfigureDrugDialog() {\n  configuringDrug = null;\n  showingConfigureDialog = false;\n}\n\nvoid ConfirmDrugDialog() {\n}\n```\n\nIf you run the application you should be able to select a drug and use a slider to indicate the quantity you'd like to order, and you should be able to cancel the configuration and go back to the main page.\n\nWe want the `OnConfirm` event to add the drug to the user's order. Add an `Order` field to the `Index` component. We have already supplied the type `Order`.\n\n```csharp\nOrder order = new Order();\n```\n\nIn the `ConfirmDrugDialog` method, add the drug to the order and close the dialog.\n\n```csharp\nvoid ConfirmDrugDialog() {\n  order.Drugs.Add(configuringDrug);\n  CancelConfigureDrugDialog();\n}\n```\n\n## Displaying the order\n\nTo display the order we have supplied a `ConfiguredDrugItem` component in the `Shared` directory of the `Client` project.\n`ConfiguredDrugItem` takes two parameters, `Drug` for what drug is shown and an event callback `OnRemoved` for when the item is removed from the order.\nAdd this markup to the [`Index.razor`](./Workshop.Client/Pages/Index.razor) markup just below the main `div`:\n\n```html\n\u003cdiv class=\"sidebar\"\u003e\n  @if (order.Drugs.Any())\n  {\n    \u003cdiv class=\"order-contents\"\u003e\n      \u003ch2\u003eYour order\u003c/h2\u003e\n\n      @foreach (var configuredDrug in order.Drugs)\n      {\n        \u003cConfiguredDrugItem Drug=\"configuredDrug\" OnRemoved=\"@(() =\u003e RemoveConfiguredDrug(configuredDrug))\" /\u003e\n      }\n    \u003c/div\u003e\n  }\n  else\n  {\n    \u003cdiv class=\"empty-cart\"\u003eChoose a drug\u003cbr\u003eto get started\u003c/div\u003e\n  }\n\n  \u003cdiv class=\"order-total @(order.Drugs.Any() ? \"\" : \"hidden\")\"\u003e\n    Total:\n    \u003cspan class=\"total-price\"\u003e@order.GetFormattedTotalPrice()\u003c/span\u003e\n    \u003cbutton class=\"btn btn-warning\" disabled=\"@(order.Drugs.Count == 0)\" @onclick=\"@PlaceOrder\"\u003e\n      Order \u003e\n    \u003c/button\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n```\n\nIn the `@code` section you also need to add event handlers for the events specified in the markup above.\nIn the `RemoveConfiguredDrug` handler we simply remove the drug from the order. In the `PlaceOrder` handler we post the order so it's saved to the database and reset the current order.\n\n```csharp\nvoid RemoveConfiguredDrug(Drug drug)\n{\n  order.Drugs.Remove(drug);\n}\n\nasync Task PlaceOrder()\n{\n  await HttpClient.PostJsonAsync(\"orders\", order);\n  order = new Order();\n}\n```\n\nRun the application and you should now be able to add drugs to your order and see the total in the sidebar to the right.\nClicking the `Order` button will save the order to the database but currently there is nothing in the user interface that indicates it has happened. Check out the next step to find out how to display the user's orders!\n\n## Stage 3\n\nIn this stage we want to be able to show the users order status and let them track their order on a map.\n\n### Show order status\n\nTo show the order status we need to add a navigation link.\nIn [`Shared/MainLayout.razor`](./Workshop.Client/Shared/MainLayout.razor) on the client, add a NavLink component after the existing one.\n\n```html\n\u003cNavLink href=\"myorders\" class=\"nav-tab\"\u003e\n  \u003cimg src=\"img/bike.svg\" /\u003e\n  \u003cdiv\u003eMy Orders\u003c/div\u003e\n\u003c/NavLink\u003e\n```\n\nNext we need to create the `myorders` component. Create a file called `MyOrders.razor` in the `Pages` directory on the client and add the following code:\n\n```html\n@page \"/myorders\"\n\u003cdiv class=\"main\"\u003e\n  My orders will go here\n\u003c/div\u003e\n```\n\nNotice how we define the route at the top which corresponds with the `href` attribute on the `NavLink` component in `MainLayout.razor`. This is how Blazor knows which component to load.\n\nStart the application and you should have a new tab at the top. When clicking it you should see the message \"_My orders will go here_\".\n\nTo display a list of orders we again need to inject the http client into our `MyOrders` component and add a code block which requests the data we need and stores it in a local field.\n\n```csharp\n@inject HttpClient HttpClient\n```\n\nand\n\n```csharp\n@code {\n  List\u003cOrderWithStatus\u003e ordersWithStatus;\n\n  protected override async Task OnParametersSetAsync()\n  {\n    ordersWithStatus = await HttpClient.GetJsonAsync\u003cList\u003cOrderWithStatus\u003e\u003e(\"orders\");\n  }\n}\n```\n\nWe need to make the UI display different output in different cases:\n\n1. While we're waiting for data to load\n2. If it turns out that the user has never placed any orders\n3. If the user has placed one or more orders\n\nLet's update the `MyOrders`'s markup inside the main `div` to reflect this:\n\n```csharp\n@if (ordersWithStatus == null)\n{\n  \u003ctext\u003eLoading...\u003c/text\u003e\n}\nelse if (ordersWithStatus.Count == 0)\n{\n  \u003ch2\u003eNo orders placed\u003c/h2\u003e\n  \u003ca class=\"btn btn-success\" href=\"\"\u003eOrder some drugs\u003c/a\u003e\n}\nelse\n{\n  \u003ctext\u003eTODO: show orders\u003c/text\u003e\n}\n```\n\nThe `\u003ctext\u003e` element is not HTML or a component. It is a signal to the compiler that you want to treat the contents within the element as a string and not as C# source code.\nNext, delete the database file called `drugs.db` in the `Server` project structure and run the application to show the message that no orders are placed.\n\n### Showing a grid of orders\n\nNow we want to show all the orders to the user. Replace the `TODO` above with the following code where the component `OrderItem` is given to you.\n\n```html\n\u003cdiv class=\"list-group orders-list\"\u003e\n  @foreach (var item in ordersWithStatus) {\n  \u003cOrderItem OrderWithStatus=\"item\" /\u003e\n  }\n\u003c/div\u003e\n```\n\nFeel free to check out or change things on the `OrderItem` component in the `Shared` directory.\nRun the application, place an order and go to the `My Orders` tab. You will see your order with status, items, and total.\n\nNext we want to enable the user to see their order details and track their order on a map.\n\n### Order details and tracking\n\nWe have provided the more complicated parts of this component.\nHave a look at [`Pages/OrderDetails.razor`](./Workshop.Client/Pages/OrderDetails.razor). This is most of the logic needed to show the order information.\n\nWhat it does is:\n\n- Hook into the `OnParametersSet` lifecycle method which runs both when the component is instantiated and when the parameters change\n- We start polling for updates on the order, cancelling any pending polling for other orders.\n- Every 4 seconds, poll the backend for an update to the order.\n- If the order is invalid we cancel the polling and prints a message telling the user\n- If the order is valid we show when the order is placed and its status.\n- Call the `StateHasChanged()` method after polling is complete to tell Blazor to rerender the component.\n\nRun the application to see the results. Place an order, go to the `My Orders` tab and click track.\nWe want to show even more on this page, so create a file called `OrderReview.razor` in the `Shared` directory and add the following markup:\n\n```html\n@foreach (var Drug in Order.Drugs) {\n\u003cp\u003e\n  \u003cstrong\u003e\n    @(Drug.Quantity) x @Drug.Deal.Name ($@Drug.GetFormattedTotalPrice())\n  \u003c/strong\u003e\n\u003c/p\u003e\n}\n\n\u003cp\u003e\n  \u003cstrong\u003e\n    Total price: $@Order.GetFormattedTotalPrice()\n  \u003c/strong\u003e\n\u003c/p\u003e\n\n@code { [Parameter] public Order Order { get; set; } }\n```\n\nBack in OrderDetails.razor, replace the TODO with the following code to use your new component:\n\n```html\n\u003cdiv class=\"track-order-details\"\u003e\n  \u003cOrderReview Order=\"@orderWithStatus.Order\" /\u003e\n\u003c/div\u003e\n```\n\nNow run your application to see a functional order details display.\nIf you are quick enough from ordering, you will see the status change live from _preparing_ to _out for delivery_ and finally _delivered_ within about one minute.\nYou can change the delivery time in the `Shared` projects [`OrderWithStatus.cs`](./Workshop.Shared/OrderWithStatus.cs) file by changing the `deliveryDuration` variable.\n\nFinally we want to let users track their orders on a map.\nTo do this we want to use JavaScript Interop which a way of calling browser APIs or existing JavaScript libraries from your blazor code.\nWe have supplied you with most of the logic to get this up and running and you mostly have to connect the dots to make it show up in your order details.\nThe logic can be found in the `ComponentsLibrary` project under `Map`.\nIn your [`_Imports.razor`](./Workshop.Client/_Imports.razor) file, add a using statement to bring the map into scope:\n\n```csharp\n@using Workshop.ComponentsLibrary.Map\n```\n\nAdd the `Map` component to the `OrderDetails` component by adding the following below the `track-order-details` `div`:\n\n```html\n\u003cdiv class=\"track-order-map\"\u003e\n  \u003cMap Zoom=\"13\" Markers=\"orderWithStatus.MapMarkers\" /\u003e\n\u003c/div\u003e\n```\n\nAnd that's it! Now you have a working drug delivery service. Check out [Blazor's](https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor) website for more info on the features it provides.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdipsas%2Fblazorworkshop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdipsas%2Fblazorworkshop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdipsas%2Fblazorworkshop/lists"}