Click here to Skip to main content
13,668,060 members
Click here to Skip to main content
Add your own
alternative version

Stats

12.2K views
252 downloads
34 bookmarked
Posted 11 May 2018
Licenced CPOL

WebAssembly with Blazor

Rate this:
Please Sign up or sign in to vote.
Developing Single Page Applications with C# Code and Blazor

 

Click the image to run test the game online!

Introduction

Have you ever wanted to develop and run code for the client side, with programming languages other than JavaScript?

This article uses a bricks game written (almost) entirely in C#, and with the help of Blazor project we will explore this brave new world of the technology called WebAssembly, discussing what it does and what it does not, how it interacts with JavaScript and how it is generated from the good old C# language.

Background

In the recent years, C# language gained new territories due to the release of some interesting technologies and tools, to name a few:

  • Xamarin, a tool aimed at cross-platform development for Windows, Android and iOS
  • .NET Core, a cross-platform framework running on Windows, Linux or Mac
  • ASP.NET Core, a cross-platform web development framework
  • Visual Studio Code, a lightweight development environment for working with many languages, C# included.

All these tools and frameworks ended up expanding the interest for the C# language.

And now (April 2018), Microsoft is sponsoring an experiment with a new technology that will push further away the limits for C# language applications: the Blazor project.

Many years ago I developed a simple Tetris-like game engine in C#, which I used in some projects and articles here in Code Project.

When I first heard of the Blazor project, some months ago, I was excited to see how it worked. It was said that Blazor was a Single Page Application (SPA) framework, and the use of WebAssembly produced programs 30 times faster than JavaScript on the browser.

Most of the examples using Blazor I've seen so far include some simple pages, buttons and forms. So I decided to investigate whether it would work with my old Bricks game.

WebAssembly as Client Side Code

If you already wrote client-side code for the web, you know that you have a vast quantity of frameworks and libraries, such as jQuery, ReactJS, Angular, Vue.js and many others. But you always end up writing JavaScript code, which is a very flexible language, although has its own problems. You can write TypeScript code if you want to bypass some of the issues of JavaScript language, but in the end it all becomes JavaScript code.

Bringing C# to client-side development to be compiled as WebAssembly has clear advantages;

  • C# is a powerful, feature-rich and robust language, with a huge developer community. 
  • Developers can reuse existing C# code in the client. Not only their own code, but code from others. (In fact, one of the motives for me to create this project in Blazor and writing this article is the fact that I was successful in reusing existing code)
  • ASP.NET Core is powerful development framework for the web, and since it uses C# code in the server-side, it would make a lot of sense if C# was used also in the client-side, because this would mean we have a common stack to develop.

What is Blazor?

Blazor is an experimental project created by Steve Sanderson from Microsoft as a Single Page Application (SPA) framework, intended to compile C# code into WebAssembly. 

WebAssembly is a W3C specification for a binary format running on web browsers, and it is supported and implemented by all popular browsers. And since this specification was published, people have been busy creating compilers from many languages into WebAssembly binary files.

It is said that WebAssembly runs up to 30 times faster than JavaScript code, due to its near-native performance. But the concept behind Blazor is not to replace JavaScript entirely, but to complement it instead.

We must keep in mind that some JavaScript capabilities cannot be performed by Blazor. As an example. Blazor cannot access directly HTML DOM elements. Instead, Blazor was created as a component-centered framework. Components here are more conceptual than HTML elements that they will generate.  So it has to create and manipulate its own components, which in turn produce HTML fragments/elements.

Blazor was inspired in client-side SPA (Single Page Application) frameworks, such as Angular, React.JS and Vue.js, which also share the concept of components.

A project compiled with Blazor runs on the browser over the WebAssembly implementation of the .NET Framework called Mono, which in turn runs on WebAssembly. Some people wonder why the Blazor team chose Mono project instead of .NET Core, and the answer is that, unlike Mono, .NET Core does not contains presentation logic needed to work with different devices and user interfaces.

Razor

Razor was born as an engine that runs on the server and combines C# and HTML templates to generate dinamically the final HTML code that is deployed to the browser.

Blazor, on the other hand, uses Razor during compilation time as a mechanism that combines C# and HTML templates, and as a result generates C# code.

You can see in the picture below how our Index.cshtml Blazor template file was compiled into the Index.g.cs file.

We Have C# Code Generated From HTML. What Now?

But how does C# code is recognized by the browser? In fact, it is not, because the browser only knows how to run WebAssembly (.wasm files), which is a specification published by the W3C (World Wide Web Consortium).

So, how does the browser runs our C# code? First, our code is compiled into a .dll, which is a managed code. Blazor will not only deploy your application's .dlls to the browser, but also Mono must be downloaded by the browser.

Mono is an open source of the .NET Framework, used in cross-platform development tools such as Xamarin. The Mono team managed to port Mono to WebAssembly in the form of a Mono.wasm file, which is deployed to the browser. Then Mono IL (Intermediate Language) is used to run our application's managed code.

You can see by the image below how all these packages are loaded by the web browser booting process:

  1. First, blazor.js is loaded
  2. Blazor.js uses Mono's JavaScript library (mono.js)
  3. Mono.js loads Mono WebAssembly runtime (mono.wasm)
  4. Mono.wsm, in turn, loads our application DLLs (BlazorBricks and BlazorBricks.Core) and the .NET Framework DLLS.

HTML Generation

Years ago, before HTML5 and CSS3, web applications were much more limited, so people used browser plugins like Flash and Silverlight to provide a more interactive user experience. Flash used a language called ActionScript and Silverlight used C#/VB.NET in the client side. Thanks to HTML5, CSS3 and ES6, both technologies are pretty much dead today.

Because Blazor also uses C# code for the client side, it may seem that Blazor is some sort of Silverlight revival. But fortunately this is not the case. Silverlight had its own internal render mechanism that used the browser window as a canvas to draw its own interface components.

On the other hand, Blazor relies on browser's Document Object Model (DOM) to present the web pages. But C# alone can't acccess DOM directly. Instead, it must rely on JavaScript code to manipulate divs, inputs, spans and other DOM elements. You can see how this works by the diagram below:

  1. A hierarchical structure containing UI componentes to be displayed (that is, a render tree) is created by the Blazor's C# code, an then the tree is passed to the JavaScript code part of Blazor.
  2. The Blazor's JavaScript code performs the changes in the DOM  according to the structure and contents of the render tree.
  3. The Blazor JavaScript code listens to all user events, such as mouse click, key pressed, etc. and responds by invoking events implemented by C# code within the application.
  4. The C# in turn can respond by modifying some parts of the Model (or ViewModel).
  5. These changes in the model must be reflected in the view, so the JavaScript part of Blazor then analyses the render tree once again and proceed to apply only the detected differential changes to the DOM.

The Blazor Demo

You can create the standard Blazor Demo from the default Blazor project template.

For now, if you want to develop Blazor project, you are required to:

  • Download and install Visual Studio (minimum version: 15.7)
  • Search and Install Blazor component Visual Studio extension: ASP.NET Core Blazor Language Services

In order to create a new Blazor project, first choose File > New Project and then select ASP.NET Core Web App:

Next, you will be presented with various possible project types. Select Blazor:

Notice that the above image features also a Blazor - ASP.NET Core hosted type, but that kind of project is quite complex to our needs. So, let's create a new Blazor project instead.

Once I got the brand new Blazor project created, I discovered that it does not allow debugging.... at least for now. Debugging capabilities will show up later, according to the Blazor's team roadmap. So, you should press CTRL+F5 ("Start without debugging") and wait for the server to be launched and the Blazor application to run.

Then the Next step was to create a new class library project to work with Blazor. I just created a new .NET Standard project, which I called "BlazorBricks.Core", and added a reference to it in the first project.

This was interesting. As you can see in the image below, when you compile the projects you see how Blazor includes the new .NET Standard .dll as part of the Blazor project /bin folder.

The Blazor Bricks Game

Now let's explain how the game implements Blazor concepts, one piece at a time.

Layout

If you use multiple pages, you should maintain you site layout consistent as the user navigates through pages. The layout in a Blazor project is defined by the component that implements ILayoutComponent. By default, this role is played by the MainLayout.cshtml file.

@implements ILayoutComponent

@Body

@functions {
    public RenderFragment Body { get; set; }
}

Notice that the Body property is passed as a fragment to the MainLayout, which in turn will render the HTML body in the appropriate place. Here I have stripped the MainLayout.cshtml file of any HTML code, but the default and original MainLayout file code contains HTML that is shared across multiple pages.

Lifecycle

When you create and compile a Blazor page (in a .cshtml file), it generates a C# class representation of that page (inside the ./obj/Debug/netstandard2.0/Pages folder). For example, our game implements the Index.cshtml file, which is converted into a class in the Index.g.cs file. The class inherits from Microsoft.AspNetCore.Blazor.Components.BlazorComponent.

It should be noted that only the binary representation of the page is deployed to the browser. The .cshtml file is not included in the deployment. 

Each Blazor page is a BlazorComponent, and as such it follows the component lifecycle: first, it receives parameters from its parent in the render tree. Then, the OnInitAsync method of the Index.cshtml page is invoked. But how do we implement this method in C# inside the Blazor .cshtml file? We must include our C# code for that page in a @functions directive:

@functions {
    protected override async Task OnInitAsync()
    {

    }
}

Now we can implement the C# code for our game. The first step is to create a field that will provide the model for the page. This  Let's call it boardViewModel.

@functions {
    BlazorBricks.Core.BoardViewModel boardViewModel;

    protected override async Task OnInitAsync()
    {
       boardViewModel = BlazorBricks.Core.GameManager.Instance.CurrentBoard;
    }
}

Notice that we assigned to the field an instance of BlazorBricks.Core.BoardViewModel, that instance is provided by the singleton property BlazorBricks.Core.GameManager.Instance that comes from the BlazorBricks.Core project.

Data Binding

Now that you have the data source, it's time to bind it to the view.

For those who already know Razor, this will be quite straightforward, because Razor in Blazor is much like it is in ASP.NET. For instance, let's say you want to render the game score value from the boardViewModel inside HTML code. That value is provided by the Score property of the boardViewModel object. You can simply put the C# expression @boardViewModel.Score expression in the place where it should be rendered:

<div class="statsLine">
    <div>SCORE</div>
    <div>@boardViewModel.Score</div>
    <hr />
</div>

Now you can follow the same logic to display the rest of the game stats: Hi Score, Lines and Level:

<div class="statsLine">
   <div>SCORE</div>
   <div>@boardViewModel.HiScore</div>
   <hr />
</div>
<div class="statsLine">
   <div>SCORE</div>
   <div>@boardViewModel.Lines</div>
   <hr />
</div>
<div class="statsLine">
   <div>SCORE</div>
   <div>@boardViewModel.Level</div>
   <hr />
</div>

But wait... notice how the HTML code above repeats itself. You may be wondering if there is something we can do about it. Fortunately, Blazor provides the concept of components,

Components

Components allows us to encapsulate HTML fragments - like the ones above - and parameterize them. Blazor components are quite easy to implement. First create a new .cshtml with the component name you want in the /Shared folder. In our case, the component will be called StatsInfo. Then copy and paste the HTML fragment you want to be rendered by the component.

<div class="statsLine">
    <div>SCORE</div>
    <div>@boardViewModel.Score</div>
    <hr />
</div>

Then we must parameterize the component. Notice we have two variable data here; the status label and its value. Let's replace the sections with these parameters:

<div class="statsLine">
    <div>@Label</div>
    <div>@Value</div>
    <hr />
</div>

Now those parameters must come from somewhere. Again, we create a @functions section at the bottom of the document, like we did in the Index page, and then we provide 2 properties for the label and value parameters:

<div class="statsLine">
    <div>@Label</div>
    <div>@Value</div>
    <hr />
</div>

@functions
{
    public string Label { get; set; }
    public int Value { get; set; }
}

And then we have a new component in the /Shared/StatsInfo.cshtml file. Let's use it in the Index.cshtml file as if it was a regular HTML element. Notice how Blazor allows us to pass the parameters in the same way as we assign values to attributes in HTML elements:

<StatsInfo Label="SCORE"

            Value="@boardViewModel.Score" />

<StatsInfo Label="HI SCORE"

            Value="@boardViewModel.HiScore" />

<StatsInfo Label="LINES"

            Value="@boardViewModel.Lines" />

<StatsInfo Label="LEVEL"

            Value="@boardViewModel.Level" />

Fortunately, we can also create hierarchies where different types of components can be nested. Let's see how the main bricks board is displayed with the help component nesting:

<div class="board">
    <BricksBoard Bricks="@boardViewModel.Bricks" />
</div>

In the above code, @boardViewModel.Bricks provides the bricks array to be displayed as the game bricks board. But what do we find in BricksBoard.cshtml file?

@foreach (var brick in Bricks)
{
    <Brick Color="@brick.Color" />
}

@functions
{
    public BlazorBricks.Core.BrickViewModel[] Bricks { get; set; }
}

The foreach loop in turn displays one Brick component for each instance in the array. 

<span class="colorChip shapecolor-@(Color)"></span>

@functions
{
    public string Color { get; set; }
}

Event Binding

At the time I'm writing this article, Blazor supports only two events: onclick and onchange. The onclick event is used in the game to trigger the StartTickLoop method in the C# functions of the page when the player presses the "Start New Game" button. 

<button @onclick(StartTickLoop)>START NEW GAME</button>

The StartTickLoop, in turn, will call the method with the same name in the BricksPresenter in BlazorBricks.Core project.

public void StartTickLoop()
{
    BlazorBricks.Core.GameManager.Instance.Presenter.StartTickLoop();
}

The StartTickLoop method is called to start the game and the main game loop to move down the piece and wait for the player's movements. 

Back to events: remember when I said that only  onclick and onchange events are supported? When I was developing the game, I desperately needed to deal with arrow keys to control the falling piece in the game. But while the Blazor team still hasn't released a complete set of events, there is a workarond to handle key events with Blazor: we can interop with JavaScript in order to fill that gap. 

Interop: Calling a C#/.NET method from JavaScript

The JavaScript code below shows how to bind the onkeyup document in JavaScript, so that we can call a C# method that was compiled by Blazor into WebAssembly:

<script>

    const assemblyName = 'BlazorBricks';
    const namespace = 'BlazorBricks';
    const typeName = 'OnKeyUp';
    const methodName = 'Handler';

    const onkeyupMethod = Blazor.platform.findMethod(
        assemblyName,
        namespace,
        typeName,
        methodName
    );

    document.onkeyup = function (evt) {
        evt = evt || window.event;
        const keyCode = Blazor.platform.toDotNetString(evt.keyCode.toString());
        Blazor.platform.callMethod(onkeyupMethod, null, [keyCode]);

    };

    function onKeyUp(element, evt) {
        const char = Blazor.platform.toDotNetString(evt.key)
        Blazor.platform.callMethod(onkeyupMethod, null, [char]);
    }
</script>

Notice that the code above defines the method Handler to be called on the OnKeyUp class. This C# class is quite simple:

public static class OnKeyUp
{
    public static Action<string> Action { get; set; }
    public static void Handler(string value)
    {
        Action?.Invoke(value);
    }
}

And we define the Action code inside the OnInitAsync override method of the page's C# code:

        OnKeyUp.Action = async value =>
        {
            ConsoleKey consoleKey = (ConsoleKey)Enum.Parse(typeof(ConsoleKey), value);

            var presenter = BlazorBricks.Core.GameManager.Instance.Presenter;

            switch (consoleKey)
            {
                case ConsoleKey.LeftArrow:
                    presenter.MoveLeft();
                    break;
                case ConsoleKey.RightArrow:
                    presenter.MoveRight();
                    break;
                case ConsoleKey.UpArrow:
                    presenter.Rotate90();
                    break;
                case ConsoleKey.DownArrow:
                    presenter.MoveDown();
                    break;
                default:
                    break;
            }
            this.StateHasChanged();
        };

 

Porting the Old MVC Brics Game

Despite the fact that I was able to reuse my old game code, obviously it wasn't so simple to integrate the whole Bricks engine with the Blazor view. First of all, the original MVC Bricks game contained some AJAX code written in JavaScript to request server side board state with a snapshot of the bricks every fraction of a second. This was a crazy idea - I know - but it worked.

At that time, the client side JavaScript did all the work to request the board state every 1/4 of a second, thus keeping the pace of the game. But for the Blazor project I modified the Bricks engine code to make the Razor view as passive as possible, by keeping the render loop on the Bricks engine side. Now, the view side of the application only needs to start and stop the Bricks GameManager, and by doing so it changes the internal state of the game.

The Game Rules

You must be very familiar with this kind of game, but I have to explain the game rules anyway.

Here we have an empty 10 x 20 board, that is, containing 200 empty positions. As soon as the game starts, the game engine will randomly generate one new piece at a time, which will fall from the top of the board, falling at the speed of 1 square per second. When the falling piece find an obstacle (that is, part of another piece that is fixed at the bottom of the board) then it can't fall anymore, so that falling piece gets stuck. Then the game engine will produce new random pieces, and they are piled up until the pile reaches the top of the board, and at this moment the game ends. The user will have to control each falling piece, by moving it to the left, to the right, or rotating it, placing the new piece in the lowest possible empty place in the board where the new piece fits in, in a way to avoid the piled pieces to reach the top of the board. Also, when the user fills any the board rows, these rows are cleared, thus giving some extra space and prolonging the game.

The "I" shape The "L" shape The "J" shape The "O" shape
 
The "T" shape The "S" shape The "Z" shape  

The game engine can randomly generate any of the above shapes. As we can see, each shape is associated with a letter which resembles it.

For each cleared line, the user scores a total of 10 multiplied by the game level. That is, each row cleared in the first level will give 10 points. The second level will give 20 points per cleared row, and so on.

Each level is completed when the user has cleared 10 rows. That is, to reach the 5th level, the user must have cleared 40 rows.

When the game is over, the game score is compared with the previous high score, and replaces it if there is a new record.

The Next piece gives the user the opportunity to place the current piece in a way that makes it easier to accomodate the next falling piece.

The Model

The model is defined by the BoardViewModel and BrickViewModel classes, and contains all information needed by the View to render the game board, the score board and to know whether the game is over or not. As we can see below, most of the properties of BoardViewModel class are native types, except for the Bricks and Next properties, which are 2-dimensional arrays of the BrickViewModel and hold the data for the bricks and empty spaces that forms the current snapshot of the game board and the bricks corresponding to the Next piece that will fall from the top of the game board.

The low-level BrickViewModel class has informations about each individual brick: row, column and the color name. These values will be used by the View to find the corresponding divs and update their background color accordingly.

public class BrickViewModel
    {
        public int Row { get; set; }
        public int Col { get; set; }
        public string Color { get; set; }
    }

    public class BoardViewModel
    {
        public BoardViewModel()
        {
            IsGameOver = false;
        }

        public BrickViewModel[] Bricks { get; set; }
        public int Score { get; set; }
        public int HiScore { get; set; }
        public int Lines { get; set; }
        public int Level { get; set; }
        public BrickViewModel[] Next { get; set; }
        public bool IsGameOver { get; set; }
    }
}

The Game Manager

The GameManager is a class that holds all methods needed by the BricksController so that the BricksViewrequests can be communicated to the game engine, and the responses can be sent back to the BricksView.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MVCBricks.Core
{
    public class GameManager : MVCBricks.Core.IView
    {
        private static GameManager instance = null;
        private static BricksPresenter presenter = null;
        private static BoardViewModel currentBoard = null;

        private GameManager()
        {
            currentBoard = new BoardViewModel();
            currentBoard.Bricks = new BrickViewModel[] { };

            presenter = new BricksPresenter(this);
            presenter.InitializeBoard();
            presenter.Tick();
        }

When the DisplayScore is called, all the scoreboard data are gathered so that they can be consumed by the view through a single call from the controller.

public void DisplayScore(int score, int hiScore, int lines,
int level, MVCBricks.Core.Shapes.IShape next)
{
    currentBoard.Score = score;
    currentBoard.HiScore = hiScore;
    currentBoard.Lines = lines;
    currentBoard.Level = level;
    currentBoard.Next = GetBricksArray(next.ShapeArray.GetUpperBound(1) + 1,
    next.ShapeArray.GetUpperBound(0) + 1, next.ShapeArray);
}

The GetBricksArray method converts both the game board bricks array and the next shape array into a system of colors which the view can understand.

private BrickViewModel[] GetBricksArray(int rowCount, int colCount, IBrick[,] array)
{
    var bricksList = new List<BrickViewModel>();

    for (var row = 0; row < rowCount; row++)
    {
        for (var col = 0; col < colCount; col++)
        {
            var b = array[col, row];
            if (b != null)
            {
                bricksList.Add(new BrickViewModel()
                {
                    Row = row,
                    Col = col,
                    Color = b.Color.ToString().Replace("Color [", "").Replace("]", "")
                });
            }
            else
            {
                bricksList.Add(new BrickViewModel()
                {
                    Row = row,
                    Col = col,
                    Color = "rgba(0, 0, 0, 1.0)"
                });
            }
        }
    }
    return bricksList.ToArray();
}           

Further reading

Conclusion

I hope you have enjoyed reading the article, even if just to get a little glimple about Blazor. Obviously, since at this point in time Blazor is still in pre-alpha stage, it has a lot of issues to solve and improvements to make, but I see a huge potential in Blazor in inviting so many C# developers to WebAssembly development.

Please download the source code in the link at the top of the article. Do you like it? Is there any suggestions, tips or complaints about it? Please leave a comment in the section below,  I’m eager to read your feedback! :-)

History

2018-05-11: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author


You may also be interested in...

Comments and Discussions

 
QuestionNo J-shape Pin
Emil Steen25-Jun-18 3:15
memberEmil Steen25-Jun-18 3:15 
AnswerRe: No J-shape Pin
Emil Steen25-Jun-18 4:14
memberEmil Steen25-Jun-18 4:14 
GeneralMy vote of 5 Pin
tbayart25-Jun-18 1:45
membertbayart25-Jun-18 1:45 
Questionvery nice Pin
BillW3318-Jun-18 3:25
professionalBillW3318-Jun-18 3:25 
AnswerRe: very nice Pin
Marcelo Ricardo de Oliveira20-Jun-18 6:08
memberMarcelo Ricardo de Oliveira20-Jun-18 6:08 
GeneralMy vote of 5 Pin
Afzaal Ahmad Zeeshan16-Jun-18 8:52
mvpAfzaal Ahmad Zeeshan16-Jun-18 8:52 
GeneralRe: My vote of 5 Pin
Marcelo Ricardo de Oliveira20-Jun-18 6:08
memberMarcelo Ricardo de Oliveira20-Jun-18 6:08 
GeneralMy vote of 5 Pin
Igor Ladnik9-Jun-18 5:40
professionalIgor Ladnik9-Jun-18 5:40 
GeneralRe: My vote of 5 Pin
Marcelo Ricardo de Oliveira20-Jun-18 6:07
memberMarcelo Ricardo de Oliveira20-Jun-18 6:07 
QuestionExtraordinary article Pin
Mike Hankey8-Jun-18 4:26
professionalMike Hankey8-Jun-18 4:26 
AnswerRe: Extraordinary article Pin
Marcelo Ricardo de Oliveira20-Jun-18 6:07
memberMarcelo Ricardo de Oliveira20-Jun-18 6:07 
Questionfine article indeed Pin
Sacha Barber8-Jun-18 1:10
mvpSacha Barber8-Jun-18 1:10 
AnswerRe: fine article indeed Pin
Marcelo Ricardo de Oliveira20-Jun-18 6:06
memberMarcelo Ricardo de Oliveira20-Jun-18 6:06 
Questionquick questions Pin
Super Lloyd14-May-18 15:01
memberSuper Lloyd14-May-18 15:01 
AnswerRe: quick questions Pin
Marcelo Ricardo de Oliveira15-May-18 7:53
memberMarcelo Ricardo de Oliveira15-May-18 7:53 
GeneralRe: quick questions Pin
Super Lloyd15-May-18 15:39
memberSuper Lloyd15-May-18 15:39 
SuggestionAs imagens não estão funcionando Pin
Silvério Miranda14-May-18 11:32
memberSilvério Miranda14-May-18 11:32 
GeneralRe: As imagens não estão funcionando Pin
Marcelo Ricardo de Oliveira15-May-18 7:45
memberMarcelo Ricardo de Oliveira15-May-18 7:45 
QuestionUpdate BlazorBricks to Blazor 0.3 and ASP.NET Core 2.1 RC Pin
Maher Jendoubi12-May-18 12:45
memberMaher Jendoubi12-May-18 12:45 
AnswerRe: Update BlazorBricks to Blazor 0.3 and ASP.NET Core 2.1 RC Pin
Marcelo Ricardo de Oliveira15-May-18 7:44
memberMarcelo Ricardo de Oliveira15-May-18 7:44 
GeneralRe: Update BlazorBricks to Blazor 0.3 and ASP.NET Core 2.1 RC Pin
Maher Jendoubi15-May-18 11:45
memberMaher Jendoubi15-May-18 11:45 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web04-2016 | 2.8.180820.1 | Last Updated 11 May 2018
Article Copyright 2018 by Marcelo Ricardo de Oliveira
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid