O'Reilly logo

Programming ASP.NET MVC 4 by Hrusikesh Panda, Jess Chadwick, Todd Snyder

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 4. Client-Side Development

The Internet has come a long way from web pages consisting of simple HTML markup and JavaScript. Popular web applications such as Gmail and Facebook have transformed users’ expectations of websites: they are no longer satisfied with basic text but instead demand rich, interactive experiences that rival those provided by native desktop applications. As users’ demands grow, modern browsers fight to keep up and do their best to implement features and specifications—such as HTML 5 and CSS3—that make these kinds of applications possible.

Though most of this book focuses on the server-side aspects of developing web applications with the ASP.NET MVC Framework, this chapter takes a break to explore the fundamentals of creating rich web applications, showing how to use jQuery library to simplify client-side development.

Working with JavaScript

Browser incompatibilities have plagued web developers for decades. The differences in functionality and lack of standards between browsers have given rise to numerous client-side libraries and frameworks that attempt to address these problems by abstracting away the differences between browsers to provide a truly standard cross-browser API.

Emerging as the overwhelming favorite of these numerous libraries is the jQuery JavaScript Library, which, following its mantra of “Write less, Do more,” greatly simplifies HTML Document Object Model (DOM) traversal, event handling, animation, and AJAX interactions. As of version 3 of the ASP.NET MVC Framework, the jQuery Library is included in the ASP.NET MVC web application project templates, making it quite easy to get up and running and leverage jQuery with minimum work.

To see how jQuery helps abstract browser inconsistencies, take a look at the following code, which tries to find out the width and the height of the browser window:

var innerWidth = window.innerWidth,
    innerHeight = window.innerHeight;

alert("InnerWidth of the window is: " + innerWidth);
alert("InnerHeight of the window is: " + innerHeight);

This script shows alert dialogs with the correct height and width in most browsers, but it will throw an error in Internet Explorer 6-8. Why? Well, it turns out that these versions of Internet Explorer (IE) provide the same information with document.DocumentElement.clientWidth and document.DocumentElement.clientHeight properties instead.

So, in order to make this snippet of code work properly across all browsers, it must be tweaked to address IE’s inconsistency, as shown in the following listing:

var innerWidth, innerHeight;

// all browsers except IE < 9
if (typeof window.innerWidth !== "undefined") {
  innerWidth = window.innerWidth;
  innerHeight = window.innerHeight;
}
else {
  innerWidth = document.documentElement.clientWidth,
  innerHeight = document.documentElement.clientHeight
}

alert("InnerWidth of the window is: " + innerWidth);
alert("InnerHeight of the window is: " + innerHeight);

With these changes in place, the script now works correctly in all major browsers.

Due to noncompliance with or different interpretations of W3C standards, browsers are full of quirks like this. Older browsers are notorious for not complying with the standards, or complying only partially. And while newer specifications like HTML 5 and CSS3 are still in a draft state, modern browsers are rushing to provide draft implementation of these by using their own vendor-specific twists. Imagine factoring all of these variations into your application for almost every single DOM element that you might access—not only would your code become lengthy and unwieldy, but it would constantly need updating as browsers and standards evolve and gaps in the specification are plugged, leading to a maintenance nightmare.

A good way to isolate your application code from such inconsistencies is to use a framework or library that acts as a layer between your application and the DOM access and manipulation code. jQuery is an excellent and lightweight framework that greatly reduces this friction. jQuery’s simple APIs make accessing and manipulating the DOM easy and intuitive and reduce the amount of code you need to write, allowing you to focus on your application’s functionality rather than worrying about browser inconsistencies and writing repetitive boilerplate code.

Consider the snippet we just looked at, rewritten using jQuery:

var innerWidth = $(window).width(),
    innerHeight = $(window).height();

alert("InnerWidth of the window is: " + innerWidth);
alert("InnerHeight of the window is: " + innerHeight);

This code looks fairly similar to the pure JavaScript code, with the following minor changes:

  • The window object is wrapped in the special $() function, which returns a jQuery-fied object (more on this special function later).

  • It makes function calls to .width() and .height() instead of accessing the .height and .width properties.

You can see the benefits of using jQuery—the code is quite similar to regular JavaScript, making it easier to learn and understand, yet it’s powerful enough to abstract away all cross-browser issues. Furthermore, with all of our cross-browser headaches taken care of by jQuery, the amount of code required to achieve the same functionality is reduced to just one line per property.

Not only does jQuery make it easy to get property values, it also makes it easy to set them:

// set the width to 480 pixels
$(window).width("480px");

// set the height to 940 pixels
$(window).height("940px");

Notice that the same functions are used to both set and get property values, the only difference being that the setter function call takes a parameter with the new value. Using a single API in different ways to get and set values makes the jQuery syntax easy to remember as well as easier to read.

Selectors

The first step in manipulating DOM elements is to get a reference to the desired element. You can do this in many ways: through its ID, its class name, or one of its attributes, or by using JavaScript logic to navigate the DOM’s tree structure and manually locate the element.

For example, the following shows how to use standard JavaScript code to search for a DOM element by its ID:

<div id="myDiv">Hello World!</div>

<script type="text/javascript">
  document.getElementById("myDiv").innerText = "Hello jQuery";
</script>

In this simple example, you get a reference to the <div> element by calling the document.getElementById() method, passing it the ID element’s ID, and then changing the inner text to “Hello jQuery”. This code will work exactly the same way in every browser, because document.getElementById() is part of the JavaScript language and is supported by all major browsers.

Consider another scenario where you want to access an element by its class name:

<div class="normal">Hello World!</div>

<script type="text/javascript">
  document.getElementsByClassName("normal")[0].innerText = "Hello jQuery";
</script>

This seems straightforward—instead of document.getElementById(), you use document.getElementsByClassName() and access the first element of the array to set the innerText property. But wait, where did the array come from? document.getElementsByClassName() returns an array containing all elements that have the same class name. Luckily, in the example above we have only one <div>, so we know that the first element is the one we’re looking for.

In a real-world application, though, the page will likely contain several elements that may or may not have IDs, and there may be more than one element with the same class name (and some elements without any class name at all). Elements will be nested in container elements such as <div>, <p>, and <span>, as per the page design. Since the DOM is nothing but a hierarchal tree structure, what you end up having is elements nested inside one another and everything nested inside the root: document.

Consider the following example:

<div class="normal">
  <p>Hello World!</p>
  <div>
    <span>Welcome!</span>
  </div>
</div>

To access the <span> and change its content, you would have to grab the outermost <div> (having class="normal"), traverse through its child nodes, check each node to see if it is a <span>, and then do some manipulation on the <span>.

A typical JavaScript code to grab the <span> would look like:

var divNode = document.getElementsByClassName("normal")[0];

for(i=0; i < divNode.childNodes.length; i++) {
  var childDivs = divNode.childNodes[i].getElementsByTagName("div");
  for(j=0; j < childDivs.childNodes.length; j++) {
    var span = childDivs.childNodes[j].getFirstChild();
    return span;
  }
}

All this code to grab just one <span>! Now what if you wanted to access the <p> tag? Can this code be reused? Certainly not, because the <p> element is at a different node in the tree. You would need to write similar code to grab the <p> element, or tweak it with conditions to find that element. And what if there were other <span> tags within the child <div>? How would you get to a specific <span>? Answering these questions will make this code grow bigger and bigger, as you fill it with all sorts of conditional logic.

What if you then wanted to repeat this exercise in a different place that has a slightly different structure? Or if, in the future, the markup changes a little bit, altering the structure? You’d have to adjust all of your functions to take the new hierarchy into account. Clearly, if we continue in this manner, the code will soon become unwieldy, lengthy, and invariably error prone.

jQuery selectors help us tidy up the mess. By using predefined conventions, we can traverse the DOM in just a few lines of code. Let’s take a look now to see how these conventions reduce the amount of code needed to perform the same actions as in the previous examples.

Here’s how we can rewrite the traversing logic with jQuery selectors. Selecting an element by ID becomes:

$("#myDiv").text("Hello jQuery!");

We call jQuery’s $() function, passing in a predefined pattern. The “#” in the pattern signifies an ID selector, so the pattern #myDiv is the equivalent of saying document.getElementById("myDiv").

Once you get a reference to the element, you can change its inner text via jQuery’s text() method. This is similar to setting the innerText property, but it’s less verbose.

An interesting thing to note here is that almost all jQuery methods return a jQuery object, which is a wrapper around the native DOM element. This wrapping allows for “chaining” of calls—e.g., you can change the text and the color of an element in one go by saying:

$(".normal > span")       // returns a jQuery object
  .contains("Welcome!")   // again returns a jQuery object
  .text("...")            // returns a jQuery object again
  .css({color: "red"});

Because each call (.text(), .css()) returns the same jQuery object, the calls can be made successively. This style of chaining calls makes the code “fluent,” which makes it easy to read, and since you do not have to repeat the element access code, the amount of overall code that you write is reduced.

Similar to the ID pattern, selecting by class name becomes:

$(".normal").text("Hello jQuery!");

The pattern for a class-based selector is ".className".

Note

Recall that getElementsByClassName() returns an array—in this case, jQuery will change the text on all elements in array! So, if you have multiple elements with the same class name, you need to put additional filters in place to get to the right element.

Now, let’s see how easy it is to access elements with a parent-child relation and grab the <span> from our original example:

$(".normal > span").text("Welcome to jQuery!");

The “>” indicates the parent > child relation. We can even filter based on the content of the span (or any element):

$(".normal > span").contains("Welcome!").text("Welcome to jQuery!");

The .contains() filters out elements that contain the specified text. So, if there are multiple spans, and the only way to differentiate (in the absence of ID, class name, etc.) is by checking for content, jQuery selectors make that easy, too.

jQuery offers many more selector patterns. To learn about them, check out the jQuery documentation site.

Responding to Events

Every DOM element on the HTML page is capable of raising events, such as “click,” “mouse move,” “change,” and many more. Events expose a powerful mechanism to add interaction to the page: you can listen for events, and perform one or more actions in response, enhancing the user experience.

For example, consider a form with many fields. By listening for the onClick event on the Submit button, you can perform validation on the user’s input and show any error messages without refreshing the page.

Let’s add a button that alerts “hello events!” when clicked. In traditional HTML/JavaScript, the code would look like:

<input id="helloButton" value="Click Me" onclick="doSomething();">

<script type="text/javascript">
  function doSomething() {
    alert("hello events!");
  }
</script>

The onclick event handler (or listener) is specified in the markup: onclick="doSomething();". When this button is clicked, this code will show the standard message box displaying the text “hello events!”

You can also attach event handlers in a nonintrusive manner, like this:

<input id="helloButton" value="Click Me">

<script type="text/javascript">
  function doSomething() {
    alert("hello events!");
  }

  document.getElementById("helloButton").onclick = doSomething;
</script>

Notice how the markup does not specify the onclick behavior anymore? Here, we’ve separated presentation from behavior, attaching the behavior outside the presentation logic. This not only results in cleaner code, but also ensures that we can reuse the presentation logic and behaviors elsewhere without making many changes.

This is a very simple example to show how basic event handling works. In the real world, your JavaScript functions will look much more complicated and may do more than display a simple alert to the user.

Now, let’s look at the corresponding jQuery code for specifying event handlers:

<input id="helloButton" value="Click Me">

<script type="text/javascript">
  function doSomething() {
    alert("hello events!");
  }

  $(function() {
    $("#helloButton").click(doSomething);
  });
</script>

With jQuery, you first get a reference to the button using the $("#helloButton") selector, and then call .click() to attach the event handler. .click() is actually shorthand for .bind("click", handler).

$(function) is a shortcut that tells jQuery to attach the event handlers once the DOM is loaded in the browser. Remember that the DOM tree is loaded in a top-to-bottom fashion, with the browser loading each element as it encounters it in the tree structure.

The browser triggers a window.onload event as soon as it is done parsing the DOM tree and loading all the scripts, style sheets, and other resources. The $() listens for this event and executes the function (which is actually an event handler!) that attaches various element event handlers.

In other words, $(function(){…}) is the jQuery way of scripting:

window.onload = function() {
  $("#helloButton").click(function() {
    alert("hello events!");
  });
}

You can also specify the event handler inline, like this:

$(function() {
  $("#helloButton").click(function() {
    alert("hello events!");
  });
});

Interestingly, if you don’t pass a function to .click(), it triggers a click event. This is useful if you want to programmatically click the button:

$("#helloButton").click();  // will display "hello events!"

DOM Manipulation

jQuery offers a simple and powerful mechanism to manipulate the DOM, or alter properties of the DOM itself or any element.

For example, to alter CSS properties of an element:

// will turn the color of the button's text to red
$("#helloButton").css("color", "red");

You’ve already seen some of this in action; remember the “height” example from earlier in the chapter?

// will return the height of the element
var height = $("#elem").height();

In addition to simple manipulations like these, jQuery allows you to create, replace, and remove any markup from a group of elements or the document root with ease.

The following example demonstrates adding a group of elements to an existing <div>:

<div id="myDiv">
</div>

<script type="text/javascript">
  $("#myDiv").append("<p>I was inserted <i>dynamically</i></p>");
</script>

This results in:

<div id="myDiv">
  <p>I was inserted <i>dynamically</i></p>
</div>

It is just as easy to remove any element (or a set of elements):

<div id="myDiv">
  <p>I was inserted <i>dynamically</i></p>
</div>

<script type="text/javascript">
  $("#myDiv").remove("p");  // will remove the <p> and its children
</script>

This code results in:

<div id="myDiv">
</div>

jQuery provides several methods to control the placement of markup, as Table 4-1 illustrates.

Table 4-1. Commonly used DOM manipulation methods

MethodDescription

.prepend()

Inserts at the beginning of the matched element

.before()

Inserts before the matched element

.after()

Inserts after the matched element

.html()

Replaces all the HTML inside the matched element

AJAX

AJAX (Asynchronous JavaScript and XML) is a technique that enables a page to request or submit data without doing a refresh or postback.

Using asynchronous requests to access data behind the scenes (on demand) greatly enhances the user experience because the user does not have to wait for the full page to load. And since the full page doesn’t have to reload, the amount of data requested from the server can be significantly smaller, which results in even faster response times.

At the heart of AJAX is the XmlHttpRequest object, which was originally developed by Microsoft for use in Outlook Web Access with Exchange Server 2000. It was soon adopted by industry heavyweights such as Mozilla, Google, and Apple and is now a W3C standard (http://www.w3.org/TR/XMLHttpRequest/).

A typical AJAX request with XmlHttpRequest object would look like:

// instantiate XmlHttpRequest object
var xhr = new XMLHttpRequest();

// open a new 'GET' request to fetch google.com's home page
xhr.open("GET", "http://www.google.com/", false);

// send the request with no content (null)
xhr.send(null);

if (xhr.status === 200) {   // The 200 HTTP Status Code indicates a successful request

  // will output reponse text to browser's console (Firefox, Chrome, IE 8+)
  console.log(xhr.responseText);
}
else {  // something bad happened, log the error
      console.log("Error occurred: ", xhr.statusText);
}

This example creates a synchronous request (the third parameter in xhr.open()), which means that the browser will pause the script execution until the response comes back. You typically want to avoid these kinds of synchronous AJAX requests at all costs because the web page will be unresponsive until the response comes back, resulting in a very poor user experience.

Luckily, it’s quite easy to switch from a synchronous request to an asynchronous request: simply set the third parameter in xhr.open() to true. Now, because of the asynchronous nature, the browser will not stop; it will execute the next line (the xhr.status check) immediately. This will most likely fail because the request may not have completed executing.

To handle this situation, you need to specify a callback—a function that gets called as soon as the request is processed and a response is received.

Let’s look at the modified code now:

// instantiate XmlHttpRequest object
var xhr = new XMLHttpRequest();

// open a new asynchronous 'GET' request to fetch google.com's home page
xhr.open("GET", "http://www.google.com/", true);

// attach a callback to be called as soon as the request is processed
xhr.onreadystatechange = function (evt) {

  //  as the request goes through different stages of processing,
  //  the readyState value will change
  //  this function will be called every time it changes,
  //  so readyState === 4 checks if the processing is completed
  if (xhr.readyState === 4) {
    if (xhr.status === 200) {
      console.log(xhr.responseText)
    }
    else {
      console.log("Error occurred: ", xhr.statusText);
    }
  }
};

// send the request with no content (null)
xhr.send(null);

This code is almost identical to the synchronous version, except that it has a callback function that gets executed whenever the server sends back any information.

Note

You must attach any callbacks before issuing xhr.send(), or they will not be called.

Let’s look at the equivalent jQuery code. jQuery offers an .ajax() method and various shorthands for accomplishing common tasks using AJAX.

Here’s the jQuery version:

$.ajax("google.com")  // issue a 'GET' request to fetch google.com's home page
  .done(function(data) { // success handler (status code 200)
      console.log(data);
    })
  .fail(function(xhr) {  // error handler (status code not 200)
      console.log("Error occurred: ", xhr.statusText);
    });

The first line specifies the URL from which you want to request data. The code then specifies the callback functions for the success and error conditions (jQuery takes care of checking for readyState and the status code).

Notice how we didn’t have to specify the type of request (GET) or whether it is asynchronous or not. This is because jQuery uses GET by default and $.ajax() is asynchronous by default.

You can override these parameters (and more) to fine-tune your request:

$.ajax({
  url: "google.com",
  async: true,         // false makes it synchronous
  type: "GET",          // 'GET' or 'POST' ('GET' is the default)
  done: function(data) {   // success handler (status code 200)
          console.log(data);
        },
  fail: function(xhr) {   // error handler (status code not 200)
          console.log("Error occurred: ", xhr.statusText);
        }
});

jQuery AJAX offers many more parameters than what’s shown here. See the jQuery documentation site for details.

Note

.done() and .fail() were introduced in jQuery 1.8. If you’re using an older version of jQuery, use .success() and .error(), respectively.

Client-Side Validation

In Chapter 3, you were introduced to server-side validation techniques. In this section, you’ll see how you can enhance the user experience by performing some of the same validations purely on the client side (without making a round-trip to the server), with the help of jQuery and the jQuery validation plug-in.

ASP.NET MVC (starting with version 3) offers unobtrusive client-side validation out of the box. Client validation comes enabled by default, but you easily enable or disable it by tweaking these two settings in your web.config file:

<configuration>
  <appSettings>
    <add key="ClientValidationEnabled" value="true"/>
    <add key="UnobtrusiveJavaScriptEnabled" value="true"/>
  </appSettings>
</configuration>

The good part about performing client-side validation with the jQuery validation plug-in is that it can take advantage of the DataAnnotation attributes defined in your model, which means that you have to do very little to start using it.

Let’s revisit the Auction model from Chapter 3 to see how data annotations were used in input validation:

public class Auction
{
    [Required]
    [StringLength(50,
      ErrorMessage = "Title cannot be longer than 50 characters")]
    public string Title { get; set; }

    [Required]
    public string Description { get; set; }

    [Range(1, 10000,
      ErrorMessage = "The auction's starting price must be at least 1")]
    public decimal StartPrice { get; set; }

    public decimal CurrentPrice { get; set; }
    public DateTime EndTime { get; set; }
}

And here’s the view that renders out the validation messages:

<h2>Create Auction</h2>

@using (Html.BeginForm())
{
  @Html.ValidationSummary()

  <p>
      @Html.LabelFor(model => model.Title)
      @Html.EditorFor(model => model.Title)
      @Html.ValidationMessageFor(model => model.Title, "*")
  </p>
  <p>
      @Html.LabelFor(model => model.Description)
      @Html.EditorFor(model => model.Description)
      @Html.ValidationMessageFor(model => model.Description, "*")
  </p>
  <p>
      @Html.LabelFor(model => model.StartPrice)
      @Html.EditorFor(model => model.StartPrice)
      @Html.ValidationMessageFor(model => model.StartPrice)
  </p>
  <p>
      @Html.LabelFor(model => model.EndTime)
      @Html.EditorFor(model => model.EndTime)
      @Html.ValidationMessageFor(model => model.EndTime)
  </p>
  <p>
      <input type="submit" value="Create" />
  </p>
}

The validation we’re performing is quite simple, yet with server-side validation, the page has to be submitted via a postback, inputs have to be validated on the server, and, if there are errors, the messages need to be sent back to the client and, after a full page refresh, shown to the user.

With client-side validation, the inputs are checked as soon as they are submitted, so there is no postback to the server, there’s no page refresh, and the results are shown instantly to the user!

To begin with client-side validation, go ahead and reference the jQuery validation plug-in scripts in the view:

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")"
       type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")"
       type="text/javascript"></script>

Note

If you use Visual Studio’s “Add View” wizard to generate Create or Edit views, you may choose the “Reference script libraries” option to have Visual Studio add these references automatically. Instead of the <script> tag references shown above, however, Visual Studio will achieve the same thing through a reference to the ~/bundles/jquery-val script bundle toward the bottom of the view. See Bundling and Minification for more information about script bundling.

If you run the application now and inspect the Create Auction page’s source (using “View Source”), you’ll see the following markup being rendered (with unobtrusive JavaScript and client-side validation enabled):

<form action="/Auctions/Create" method="post" novalidate="novalidate">
  <div class="validation-summary-errors" data-valmsg-summary="true">
    <ul>
      <li>The Description field is required.</li>
      <li>The Title field is required.</li>
      <li>Auction may not start in the past</li>
    </ul>
  </div>
  <p>
    <label for="Title">Title</label>

    <input class="input-validation-error"
      data-val="true"
      data-val-length="Title cannot be longer than 50 characters"
      data-val-length-max="50"
      data-val-required="The Title field is required."
      id="Title" name="Title" type="text" value="">

    <span class="field-validation-error"
      data-valmsg-for="Title"
      data-valmsg-replace="false">*</span>
  </p>
  <p>
    <label for="Description">Description</label>

  <input class="input-validation-error"
      data-val="true"
      data-val-required="The Description field is required."
      id="Description" name="Description" type="text" value="">

    <span class="field-validation-error"
      data-valmsg-for="Description"
      data-valmsg-replace="false">*</span>
  </p>
  <p>
    <label for="StartPrice">StartPrice</label>

    <input data-val="true"
      data-val-number="The field StartPrice must be a number."
      data-val-range="The auction's starting price must be at least 1"
      data-val-range-max="10000"
      data-val-range-min="1"
      data-val-required="The StartPrice field is required."
      id="StartPrice" name="StartPrice" type="text" value="0">

    <span class="field-validation-valid"
      data-valmsg-for="StartPrice"
      data-valmsg-replace="true"></span>
  </p>
  <p>
    <label for="EndTime">EndTime</label>

    <input data-val="true"
      data-val-date="The field EndTime must be a date."
      id="EndTime" name="EndTime" type="text" value="">

    <span class="field-validation-valid"
      data-valmsg-for="EndTime"
      data-valmsg-replace="true"></span>
  </p>
  <p>
    <input type="submit" value="Create">
  </p>
</form>

With unobtrusive JavaScript and client-side validation enabled, ASP.NET MVC renders the validation criteria and corresponding messages as data-val- attributes. The jQuery validation plug-in will use these attributes to figure out the validation rules and the corresponding error messages that will be displayed if the rules are not satisfied.

Go ahead now and try to submit the form with some invalid values. You’ll see that the “submit” did not actually submit the form; instead, you’ll see error messages next to the invalid inputs.

Behind the scenes, the jQuery validation plug-in attaches an event handler to the form’s onsubmit event. Upon form submit, the jQuery validation plug-in scans through all the input fields and checks for errors against the given criteria. When it finds an error, it shows the corresponding error message.

Being unobtrusive in nature, the jQuery validation plug-in doesn’t emit any code to the page, nor is anything required on your part to wire up the client-side validation logic with the page’s events. Rather, the code to attach to the onsubmit event, as well as the validation logic, is part of the jquery.validate.js and jquery.validate.unobtrusive.js files.

The good thing about being unobtrusive is that if you forget to include these two scripts, the page will still render without any errors—only, the client-side validation will not happen client side!

This section was meant to show you how easy it is to start taking advantage of client-side validation and was kept simple and minimal purposefully. The jQuery validation plug-in is quite complex and offers many more features and customizations. You’re encouraged to learn more about the plug-in at the official documentation page.

Summary

jQuery makes cross-browser development a joyful experience. ASP.NET MVC’s support of jQuery out of the box means you can quickly build a rich and highly interactive client-side user interface (UI) with very few lines of code. With client-side validation and unobtrusive JavaScript, validating user input can be done with minimal effort. All of these techniques combined can help you develop highly interactive and immersive web applications with ease.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required