Click here to Skip to main content
15,867,308 members
Articles / Web Development / ASP.NET

How to Work with Lists and Collections of Knockout ?

Rate me:
Please Sign up or sign in to vote.
4.81/5 (6 votes)
29 Jun 2013CPOL5 min read 29.7K   12   6
How to Work with Lists and Collections of Knockout

Introduction

  • Very often, you'll want to generate repeating blocks of UI elements, especially when displaying lists where the user can add and remove elements
  • Knockout lets you do that easily, using observable Arrays and the foreach binding

What is Observable Arrays ?

  • If you want to detect and respond to changes on one object, you’d use observables
  • If you want to detect and respond to changes of a collection of things, use an observableArray
  • This is useful in many scenarios where you’re displaying or editing multiple values and need repeated sections of UI to appear and disappear as items are added and removed
e.g.
C#
var myObservableArray = ko.observableArray();// Initially an empty array
myObservableArray.push('Some value');// Adds the value and notifies observers

Important Note

  • An observableArray tracks which objects are in the array, not the state of those objects
  • Simply putting an object into an observableArray doesn’t make all of that object’s properties themselves observable
  • Of course, you can make those properties observable if you wish, but that’s an independent choice
  • An observableArray just tracks which objects it holds, and notifies listeners when objects are added or removed

How to Apply observableArray with Real world Application ? 

  • Here I have used Visual Studio 2012 and ASP.NET MVC 4 Web Application
  • Please Follow the inline comments for better understanding

CASE 1 : Displaying Reservation Data  

View's Code - Index.cshtml

HTML
<h2>Your Seat Reservations</h2>

<table>
    <thead>
        <tr>
            <th>Passenger Name</th>
            <th>Meal</th>
            <th>Amount ($)</th>
            <th></th>
        </tr>
    </thead>

    @*render a copy of seats child elements for each entry in the seats array*@
    <tbody data-bind="foreach: seats">
        <tr>
          <td data-bind="text: name"></td>
          <td data-bind="text: meal().mealName"></td>
          <td data-bind="text: meal().price"></td>
        </tr>
    </tbody>
</table> 

ViewModel (Javascript) Code - ko.list.js

C#
// Class to represent a row in the seat reservations grid
function SeatReservation(name, initialMeal) {
    var self = this; 
    self.name = name;
    self.meal = ko.observable(initialMeal);
}
// Overall viewmodel for this screen, along with initial state
function ReservationsViewModel() {
    var self = this;
    // Non-editable Meals data - would come from the server
    self.availableMeals = [
        { mealName: "Vegetarian Raw Meal", price: 10.52 },
        { mealName: "Vegetarian Vegan Meal", price: 34.95 },
        { mealName: "Fruit Platter Meal", price: 45.50 }
    ];
    // Editable data - seats Array
    self.seats = ko.observableArray([
        new SeatReservation("Sampath", self.availableMeals[0]),
        new SeatReservation("Lokuge", self.availableMeals[1])
    ]);
}
ko.applyBindings(new ReservationsViewModel());

Output

Important Points about above Code 

SeatReservation

  • a simple JavaScript class constructor that stores a passenger name with a meal selection

ReservationsViewModel, a ViewModel class

  • availableMeals - a JavaScript object providing meal data
  • seats - an array holding an initial collection of SeatReservation instances.Note that it's a ko.observableArray , which means it can automatically trigger UI updates whenever items are added or removed
meal Property
  • is an observable
  • It's important to invoke meal() as a function (to obtain its current value) before attempting to read sub-properties
  • In other words, write meal().price, not meal.price

CASE 2 : Adding items 

View's Code - Index.cshtml

@*... leave all the rest unchanged ...*@
<button data-bind="click: addSeat">Reserve Another Seat</button>

ViewModel (Javascript) Code - ko.list.js

C#
function ReservationsViewModel() {
     var self = this;
    // ... leave all the rest unchanged ...
    // add seats
    self.addSeat = function () {
        self.seats.push(new SeatReservation("Chaminda", self.availableMeals[2]));
    };
}

Output

Explanation about above Scenario 

  • Now when you click "Reserve Another Seat", the UI updates to match
  • This is because seats is an observable Array, so adding or removing items will trigger UI updates automatically
  • Note that adding a row does not involve regenerating the entire UI
  • For efficiency, Knockout tracks what has changed in your ViewModel, and performs a minimal set of DOM updates to match

CASE 3 : Edit items

View's Code - Index.cshtml

HTML
@*... leave all the rest unchanged ...*@
<tbody data-bind="foreach: seats">
  <tr>
    <td data-bind="text: name"></td>
    <td><select data-bind="options: $root.availableMeals, value: meal,
               optionsText: 'mealName'"></select></td>
    <td data-bind="text: meal().price"></td>
  </tr>
</tbody>

Output

Important Points about above Code

  • This code uses two new bindings, options and optionsText 
  • Which together control both the set of available items in a dropdown list, and which object property (in this case, mealName) is used to represent each item on screen
  • You can now select from the available meals, and doing so causes the corresponding row (only) to be refreshed to display that meal's price

CASE 4 : Formatting Prices 

View's Code - Index.cshtml

HTML
@*... leave all the rest unchanged ...*@ 
<tbody data-bind="foreach: seats">
  <tr>
   <td data-bind="text: name"></td>
   @*update the view to make use of the formattedPrice*@
   <td><select data-bind="options: $root.availableMeals, value: meal,
             optionsText: 'mealName'"></select></td>
   <td data-bind="text: formattedPrice"></td>
  </tr>
</tbody>

ViewModel (Javascript) Code - ko.list.js

C#
function SeatReservation(name, initialMeal) {
   var self = this;
  
 // ... leave all the rest unchanged ...
   
 self.formattedPrice = ko.computed(function () {
        var price = self.meal().price;
        return price ? "$" + price.toFixed(2) : "None";
    });
 }

Output 

Important Points about above Code

  • We've got a nice object-oriented representation of our data
  • So we can trivially add extra properties and functions anywhere in the object graph
  • The SeatReservation class the ability to format its own price data using some custom logic
  • Since the formatted price will be computed based on the selected meal, we can represent it using ko.computed (so it will update automatically whenever the meal selection changes)

CASE 5 : Removing Items

View's Code - Index.cshtml

<tbody data-bind="foreach: seats">
   <tr>
       @*... leave all the rest unchanged ...*@
       <td><a href="#" data-bind="click: $root.removeSeat">Remove</a></td>
   </tr>
</tbody>

ViewModel (Javascript) Code - ko.list.js

C#
function ReservationsViewModel() {
    var self = this;
   // ... leave all the rest unchanged ...
   // remove seats
   self.removeSeat = function (seat) { self.seats.remove(seat); };
}

Output

Important Points about above Code

  • Note that the $root. prefix causes Knockout to look for a removeSeat handler on your top-level ViewModel, instead of on the SeatReservation instance being bound
  • That's a more convenient place to put removeSeat in this example
  • So,I have added a corresponding removeSeat function on root ViewModel class, That is ReservationsViewModel

CASE 6 : Displaying a Total Amount

View's Code - Index.cshtml

HTML
@*... leave all the rest unchanged ...*@
<h3 data-bind="visible: totalAmount() > 0">Total Amount: $
<span data-bind="text: totalAmount().toFixed(2)"></span>
</h3>

ViewModel (Javascript) Code - ko.list.js

JavaScript
function ReservationsViewModel() {
     var self = this;
    // ... leave all the rest unchanged ...
    // Computed Total amount
    self.totalAmount = ko.computed(function () {
        var total = 0;
        for (var i = 0; i < self.seats().length; i++)
            total += self.seats()[i].meal().price;
        return total;
    });
 }

Output

Important Points about above Code

  • I have defined the total as a computed property
  • It lets the framework, take care of knowing when to recalculate and refresh the display
  • The visible binding makes an element visible or invisible as your data changes (internally, it modifies the element's CSS display style)
  • In this case, we choose to show the "Total Amount" information only if it's greater than zero
  • You can use arbitrary JavaScript expressions inside declarative bindings
  • Here, we used totalAmount() > 0 and totalAmount().toFixed(2)
  • Internally, this actually defines a computed property to represent the output from that expression
  • It's just a very lightweight and convenient syntactical alternative
  • Again, notice that since seats and meal are both observables, we're invoking them as functions to read their current values (e.g. self.seats().length)
  • When you run the code, you'll see "Total Amount" appear and disappear as appropriate, and thanks to dependency tracking, it knows when to recalculate its own value
  • You don't need to put any code in your "add" or "remove" functions to force dependencies to update manually

CASE 7 : Display the Total Number of Seats being Reserved

View's Code - Index.cshtml

HTML
<h2>Your seat reservations (<span data-bind="text: seats().length"></span>)</h2>
@*... leave all the rest unchanged ...*@
<button data-bind="click: addSeat, enable: seats().length < 3">Reserve Another Seat</button>

Output

Important Points about above Code

  • For display the Total number of seats being reserved, you can implement that in just a single place
  • You don't have to write any extra code to make the seat count update when you add or remove items
  • Just update the <h2> as above on top of your View
  • Similarly, For a Limit on the number of seats you can reserve
  • You can make the UI represent that by using the enable binding
  • The button becomes disabled when the seat limit is reached
  • You don't have to write any code to re-enable it, when the user removes some seats
  • Because the expression will automatically be re-evaluated by Knockout when the associated data changes

Final Full Code

Index.cshtml

C#
<h2>Your seat reservations (<span data-bind="text: seats().length"></span>)</h2>
<table>
    <thead>
        <tr>
            <th>Passenger Name</th>
            <th>Meal</th>
            <th>Amount ($)</th>
            <th></th>
        </tr>
    </thead>
    @*render a copy of seats child elements for each entry in the seats array*@
    <tbody data-bind="foreach: seats">
        <tr>
          <td data-bind="text: name"></td>
    @*update the view to make use of the formatted Price*@
          <td>
           <select data-bind="options: $root.availableMeals, value: meal, optionsText: 'mealName'"></select></td>
          <td data-bind="text: formattedPrice"></td>
          <td><a href="#" data-bind="click: $root.removeSeat">Remove</a></td>
        </tr>
    </tbody>
</table>
<button data-bind="click: addSeat, enable: seats().length < 3">Reserve Another Seat</button>
<h3 data-bind="visible: totalAmount() > 0">Total Amount: $<span data-bind="text: totalAmount().toFixed(2)"></span></h3>

ko.list.js 

JavaScript
// Class to represent a row in the seat reservations grid
function SeatReservation(name, initialMeal) {
    var self = this;
    self.name = name;
    self.meal = ko.observable(initialMeal);
    self.formattedPrice = ko.computed(function () {
        var price = self.meal().price;
        return price ? "$" + price.toFixed(2) : "None";
    });
}
// Overall viewmodel for this screen, along with initial state
function ReservationsViewModel() {
    var self = this;
    // Non-editable Meals data - would come from the server
    self.availableMeals = [
        { mealName: "Vegetarian Raw Meal", price: 10.52 },
        { mealName: "Vegetarian Vegan Meal", price: 34.95 },
        { mealName: "Fruit Platter Meal", price: 45.50 }
    ];
    // Editable data - seats Array
    self.seats = ko.observableArray([
        new SeatReservation("Sampath", self.availableMeals[0]),
        new SeatReservation("Lokuge", self.availableMeals[1])
    ]);
    // Computed Total amount
    self.totalAmount = ko.computed(function () {
        var total = 0;
        for (var i = 0; i < self.seats().length; i++)
            total += self.seats()[i].meal().price;
        return total;
    });
    // add seats
    self.addSeat = function () {
        self.seats.push(new SeatReservation("Chaminda", self.availableMeals[2]));
    };
    // remove seats
    self.removeSeat = function (seat) { self.seats.remove(seat); };
}
ko.applyBindings(new ReservationsViewModel());

That's It.You're Done. 

 Conclusion   

  • You saw that following the MVVM pattern makes it very simple to work withchangeable object graphs such as Arrays and Hierarchies
  • You update the underlying data, and the UI automatically updates in sync
  • So enjoy with this Great Framework 

License

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


Written By
Software Developer (Senior) Freelancer
Sri Lanka Sri Lanka
Sampath Lokuge holds a Bachelor of Science degree in the Mathematics and Computer Science at the University of Colombo, Sri Lanka.

He possesses over 8 years of experience in constructing web applications using Microsoft Technologies including ASP.net MVC, C#, SQL Server, Web API, Entity Framework and also other web technologies such as HTML5, CSS3,jQuery and AngularJS.

Sampath has earned Microsoft certifications such as MCP, MCAD, MCSD and MCTS and very recently he has completed MS (Microsoft Specialist) for MVC 4 and MCSD (Windows Store Apps Using HTML5).

Besides that, he is an active blogger, writing about web and mobile development issues and promoting best practices.He also actively participates in online communities such as Code Project and StackOverflow.He himself is handling three communities, which are ASP.net MVC 5 With C# on Linkedin,Entity Framework 6 on G+ and Hybrid Mobile App with WinJS on Facebook.

Now, I am a 100% Freelancer. Smile | :)

Tech Blogs


Sampath Lokuge Tech Universe

Communities which I'm Handling :


Entity Framework 6

ASP.net MVC 5 With C#

Hybrid Mobile App with WinJS

Comments and Discussions

 
GeneralMy 5 Pin
DamithSL14-Dec-14 5:46
professionalDamithSL14-Dec-14 5:46 
GeneralRe: My 5 Pin
Sampath Lokuge14-Dec-14 16:37
Sampath Lokuge14-Dec-14 16:37 
GeneralMy vote of 5 Pin
mrwisdom30-Jun-13 20:19
mrwisdom30-Jun-13 20:19 
GeneralRe: My vote of 5 Pin
Sampath Lokuge30-Jun-13 21:23
Sampath Lokuge30-Jun-13 21:23 
GeneralMy vote of 5 Pin
DK0928-Jun-13 6:27
DK0928-Jun-13 6:27 
GeneralRe: My vote of 5 Pin
Sampath Lokuge28-Jun-13 6:44
Sampath Lokuge28-Jun-13 6:44 

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.