For this reason, it appears to be better than both React and Angular for building modular complex systems that mix server side techniques (like Razor), with client side techniques.
Integrating Knockout.js with ASP.NET Core
In this “how to” article, I’ll show how to integrate Knockout and ASP.NET Core in several ways:
1) To build a Single Page Application that communicates with Asp.net core API controllers;
2) To define “components” for Razor based Views, like the ones we might define with React.js, but MVVM based;
3) To enhance Razor pages with client side bindings, something difficult to achieve with both Angular.js and React.js.
ASP.NET Core Webpack based SPA templates
You may run this command in any Windows, Mac, and Linux console.
After the installation run:
It should list all available project templates, as shown in the following image:
Here choose the template whose short name is “knockout”.
Create a folder called “KnockoutDemo” for our project. Then open a console in this directory. In Windows, we can do this by holding down “shift” while right clicking on the newly created folder, and then selecting "open command window here".
Once KnockoutDemo is the default folder in your console, in order to create a new knockout SPA project, type the following:
After that you may open the newly created KnockoutDemo.csproj in Visual Studio 2017.
Once all Nuget, and NPM packages have been restored, save the whole solution.
When you run the project, you should see something like this:
It is a Single Page Application with three pages. We will analyze it in the following subsection.
Structure of a Knockout.js Single Page Application
When you click a menu link, the content of the browser address bar changes, but the browser doesn’t perform any GET.
Link URLs are mapped into knockout.js components by the code in wwwroot\ClientApp\router.ts, that in turn uses Crossroad.js to handle routes (url-components mappings).
After that, knockout.js components are retrieved by the custom knockout.js component loader contained in wwwroot\ClientApp\router.ts\webpack-component-loader.ts that in turn relies on webpack 2 loader to communicate with the server.
Browser history and address bar are handled by History.js.
The whole application is hosted by a single View, namely the view rendered by the Index action method of the Home controller:
The app-root component contains the “hole” that hosts the various SPA pages with the appropriate knockout.js bindings. We will analyze it in the next section.
The layout page instead references the following:
2. the vendor.css file where webpack 2 bundles all npm modules CSS that is needed at runtime
3. The site.css file with the site-specific CSS. Actually, site.css is added only in production and staging. This is done since webpack 2 is configured to add all CSS rules within a style tag during development, so that changes made while the program is running, takes immediate effect thanks to webpack 2 hot module replacement.
Bundling files with Webpack 2
The way webpack 2 bundles all files is defined into the webpack.config.js and webpack.config.vendor.js configuration files.
webpack.config.vendor.js uses the DllPlugin to bundle all needed npm modules as a library that is then referenced by webpack.config.js with the DllReferencePlugin plugin.
webpack.config.js specifies how to bundle all js, ts, CSS and images that are specific to the project. It uses the awesome-typescript-loader plugin to compile and load TypeScript according to the compilation configuration contained in tsconfig.json.
knockout.js components are bundled in separate files and loaded on demand, thanks to the lazy loading feature of the webpack 2 'bundle-loader!' plugin.
Lazy loading is activated by prefixing the file name contained in a “require” with the 'bundle-loader?lazy!
When this is done, the call to “require” instead of returning the actual module returns a “load function”, that when called with a callback parameter, triggers the actual module load.
In our case, the actual “load function” will be invoked by the webpack-component-loader.ts custom knockout loader that we will look at in the next section.
..in webpack.config.js specifies that CSS should be bundled in the html as in-line style during development, and as a unique minimized file, in all other environments.
..causes all images referenced in CSS files and js/ts files to be inserted in-line if their size is less than 25k, and in other modules, if they exceed that size. In the next section, we will see an example of image bundling.
All plugins are referenced in the webpack 2 “plugins” section with further configurations in their constructors.
While all application specific source client files are contained in the ClientApp folder, all files bundled by webpack 2 are deployed in wwwroot/dist.
You don’t need to call webpack 2 from the command line to process all files, since the UseWebpackDevMiddlewaremiddleware in Startup.cs automatically performs this job whenever the application is started in the development mode:
The above code invokes webpack 2 with the “hot module replacement” feature.
When this feature on webpack 2 detects file changes, it automatically sends the updated modules to the browser, thus enabling the developer to see the effects of any change immediately, with no requirements of refreshing the page or restarting application debugging.
In the remainder of the article, we will analyze in detail the “pure SPA” model and how to add more SPA pages and components. Then we will see how to add other Razor Views that use knockout.js components with or without a router, and finally we will mix Razor and knockout.js code in standard views.
Implementing Knockout.js Single Page Applications
In this section, we will dive more deep into SPA specific details.
All components are defined in the ClientApp/components folder. The root component (app-root) registered in boot.ts creates a browser history object and then calls ko.applyBindings to start knockout.js:
In Index.cshtml, the “history” property is passed as parameter to the app-root component that performs the whole job of configuring the SPA engine:
The history object passed as a parameter to the app-root component is actually received by the constructor of its ViewModel defined in ClientApp\components\app-root\app-root.ts. It is used to initialize a CrossRoads based router that is defined in router.ts.
Router behavior is defined by the routes listed at the beginning of the app-root.ts :
These are passed to the router constructor, together with the history object:
The remainder of app-root.ts registers all components used by the SPA.
The nav-menu component that is used as a main menu is loaded immediately and registered with:
..whereas all other modules use a call to “require” with the lazy loading technique explained in the previous section:
When the file is processed, webpack 2 recognizes each lazy loading request and replaces the “require” call with another call. This call instead of returning a ViewModel/template pair, returns a loader function that must be called to download such a pair from the server, the first time the component is invoked.
That is why a custom knockout.js component loader that may adequately handle the loader function is registered in webpack-component-loader.ts. Since this component loader must be the preferred one, it is added at the beginning of the list of all loaders with an unshift operation:
The code above specifies a loadComponent function to be invoked immediately before the component is instantiated. It is passed the component name as first parameter, and a callback, that is called once the component is ready as the last parameter. The second parameter is the load function returned by the “require” call in the component registration.
This function is invoked and passed a lambda callback that in turn, is invoked once the component has been successfully downloaded from the server.
The lambda callback does the following:
- receives the result of the downloaded module invocation in the loadedModule parameter,
- does some processing to conform to the TypeScript module export conventions, and
- finally calls knockout.js default loader passing it loadedModule that now contains the actual ViewModel/template pair that defines the required component.
app-root template prepares the place to load components that act as SPA pages, invoking the component binding with the component name contained in the current route.
This way, when the user clicks a link, a new route becomes the current route, and the component name it contains is passed to the component binding, which in turn causes the component be downloaded from the server and instantiated.
Downloading data from a Controller
The fetch-data component scaffolded by the SPA template shows how a component may get data from an Mvc controller.
It interacts with the server with the isomorphic-fetch npm package that is a window. fetch polyfill that works both server side with node.js, as well as on client side. More specifically, it uses whatwg-fetch on client side and node-fetch on server side.
In this simple example, component data is retrieved from the server as soon as the component is loaded, so the whole data retrieval code is enclosed in the ViewModel constructor in fetch-data.ts :
On the server side, data is generated randomly:
The fetch-data template in fetch-data.html contains typical knockout.js bindings to iterate on a collection:
While data is being downloaded from the server, the forecasts property is empty, so the “ifnot” binding shows the paragraph content. Finally, when “forecast” observable is filled with data, the “foreach” binding shows the table rows bound to each item properties through the text binding.
Creating a new SPA page
In this subsection, we will see how to add a new SPA page.
As a first step, we need a new component.
Add a new folder called “about” in the “components” folder. Then create an “about.html”, and an “about.ts” files inside that folder:
Type the following in about.html:
Now write the following code in about.ts:
At the moment, our view model does nothing, since our template contains just static html with no bindings. In the next section, we will add some logic to display an image. So the TypeScript module just exports the view model / template pair.
Before using our component, we must register it.
Registration is obligatory not only for SPA pages but also for components called from within views or other components. Registration can be added in the app-root.ts file next to all other SPA pages registration:
The new SPA page is registered with lazy loading like all other pages.
Components that acts as SPA page must have a route associated with them. We may add a new route to the route list contained in the “routes” constant defined in app-root.ts:
Now our new page is working, we just need to link it somehow.
We must add a link in the main menu defined with the nav-menu component. Open nav-menu.html and add the new <li> tag below, at the end of its <ul> tag:
The css binding adds the active CSS class whenever the about page is the one currently displayed.
Run the project and click the about link. You should see the newly added page.
Adding an image to the new page
In this subsection, we will add an image to an already existing component, and will see how to bundle it with webpack 2, and how to render it with a knockout binding.
Images may be added directly to the wwwroot distribution folder and then referenced in all html files. However, you may do a require from ts files and then attach them to the Dom with the “attr” binding.
The main benefit of the second technique is that images may be preprocessed by various webpack 2 plugins. The knockout SPA template comes with the url-loader plugin that puts small images in-line instead of referencing their URLs.
However, you may use also plugins for creating responsive images.
Add an “Images” folder to the “ClentApp” folder and add the “AspNetCore.png” you can find in the source code of this article (or any other image you like):
Now require this image and insert it in a new property of the ViewModel of the component we defined in the previous subsection:
Now we may add an <img> tag to the page template:
Run the project and click the about link. You should see something like this:
Several Razor Views with Knockout.js components
A standard SPA application usually contains a single Razor view that hosts the whole SPA application, in our case the Index.cshtml.
Instead, what if you want to enhance several Razor views with knockout.js components?
Some Razor views may act as SPA connected with the browser history and with a router (like the Index.cshtml Razor View), while others may use knockout.js in a different way.
Hybrid SPA/Razor pages that mix both server side and SPA techniques offer great flexibility in practical applications because applications based entirely on SPA techniques offer a better interaction with the user but cost more, are more difficult to maintain and have a shorter life since client side techniques evolve very quickly.
Therefore, only web applications with a limited usage of SPA techniques and where a higher interaction with the user is necessary, appear quite attractive.
In this section, we will show how to build a similar scenario, by adding a new View that accesses the same contents available in the Index.cshtml view, using a bootstrap tab.
As a first step, we will modify our router so that it may properly handle relative links that point to different action methods. Open router.ts and observe the line of code below:
This code has the purpose of selecting which urls to process with the SPA router and which ones with usual http requests: all relative urls are handled by the SPA router, while complete urls are handled with http requests.
We may allow relative links to be handled with usual http requests by adding to them a “data-external=true” attribute and by modifying the previous if statement as follows:
Now we may add a new action method to the home controller: