官术网_书友最值得收藏!

Mixin' it up

In object-oriented languages, it is useful to build on one class to create a more specialized related class. For example, in the text editor, the base dialog class was extended to create an alert and confirm pop ups. What if we want to share some functionality but do not want inheritance occurring between the classes?

Aggregation can solve this problem to some extent:

class A {
  classb usefulObject;
}

The downside is that this requires a longer reference to use:

new A().usefulObject.handyMethod();

This problem has been solved in Dart (and other languages) by having a mixin class do this job, allowing the sharing of functionality without forced inheritance or clunky aggregation.

In Dart, a mixin must meet the following requirements:

  1. No constructors can be in the class declaration.
  2. The base class of the mixin must be the Object.
  3. No calls to a super class are made.

mixins are really just classes that are malleable enough to fit into the class hierarchy at any point. A use case for a mixin may be serialization fields and methods that could be required on several classes in an application and that are not part of any inheritance chain.

abstract class Serialisation {
  void save() {
  //Implementation here.
  }
  void load(String filename) {
  //Implementation here.
  }
}

The with keyword is used to declare that a class is using a mixin:

class ImageRecord extends Record with Serialisation

If the class does not have an explicit base class, it is required to specify an Object:

class StorageReports extends Object with Serialization

Note

In Dart, everything is an object, even basic types such as num are objects and not primitive types. The classes int and double are subtypes of num. This is important to know as other languages have different behaviors. Let's consider a real example of this:

main() {
int i;
print("$i");
}

In a language such as Java, the expected output would be 0; however, the output in Dart is null. If a value is expected from a variable, it is always good practice to initialize it!

For the classes Slide and SlideShow, we will use a mixin from the source file lifecyclemixin.dart to record a creation and an editing timestamp:

abstract class LifecycleTracker { 
  DateTime _created; 
  DateTime _edited; 
  recordCreateTimestamp() => _created = new DateTime.now(); 
  updateEditTimestamp() => _edited = new DateTime.now(); 
  DateTime get created => _created; 
  DateTime get lastEdited => _edited; 
} 

To use the mixin, the recordCreateTimestamp method can be called from the constructor and the updateEditTimestamp from the main edit method. For slides, it makes sense just to record the creation. For the SlideShow class, both the creation and update will be tracked.

Defining the core classes

The SlideShow class is largely a container object for a list of Slide objects and uses the mixin LifecycleTracker:

class SlideShow extends Object with LifecycleTracker {
  List<Slide> _slides; 
  List<Slide> get slides => _slides; 
...

The Slide class stores the string for the title and a list of strings for the bullet points. The URL for any image is also stored as a string:

class Slide extends Object with LifecycleTracker { 
  String titleText = ""; 
  List<String> bulletPoints; 
  String imageUrl = ""; 
... 

A simple constructor takes the titleText as a parameter and initializes the bulletPoints list.

Tip

If you want to focus on just the code when in WebStorm, double-click on the filename title of the tab to expand the source code to the entire window. Double-click again to return to the original layout.

For even more focus on the code, go to the View menu and click on Enter Distraction Free Mode.

Transforming data into HTML

To add the Slide object instance into an HTML document, the strings need to be converted into instances of HTML elements to be added to the DOM (Document Object Model). The getSlideContents() method constructs and returns the entire slide as a single object:

DivElement getSlideContents() {
  DivElement slide = new DivElement();
  DivElement title = new DivElement();
  DivElement bullets = new DivElement();

  title.appendHtml("<h1>$titleText</h1>");
  slide.append(title);

  if (imageUrl.length > 0) {
    slide.appendHtml("<img src=\"$imageUrl\" /><br/>");
  }

  bulletPoints.forEach((bp) {
    if (bp.trim().length > 0) {
      bullets.appendHtml("<li>$bp</li>");
    }
  });

  slide.append(bullets);

  return slide;
}

The Div elements are constructed as objects (instances of DivElement), while the content is added as literal HTML statements. The method appendHtml is used for this particular task as it renders HTML tags in the text. The regular method appendText puts the entire literal text string (including plain unformatted text of the HTML tags) into the element.

So, what exactly is the difference? The method appendHtml evaluates the supplied HTML and adds the resultant object node to the nodes of the parent element, which is rendered in the browser as usual. The method appendText is useful, for example, to prevent user-supplied content affecting the format of the page and preventing malicious code being injected into a web page.

Editing the presentation

When the source is updated, the presentation is updated via the onKeyUp event. This was used in the text editor project to trigger a save to local storage.

This is carried out in the build method of the SlideShow class, and follows the pattern we discussed in parsing the presentation:

build(String src) {
  updateEditTimestamp();
  _slides = new List<Slide>();
  Slide nextSlide;

  src.split("\n").forEach((String line) {
    if (line.trim().length > 0) {

      // Title - also marks start of the next slide.
      if (line.startsWith("#")) {
        nextSlide = new Slide(line.substring(1));
        _slides.add(nextSlide);
      }
      if (nextSlide != null) {
        if (line.startsWith("+")) {
          nextSlide.bulletPoints.add(line.substring(1));
        } else if (line.startsWith("!")) {
          nextSlide.imageUrl = line.substring(1);
        }
      }
    }
  });
}

As an alternative to the startsWith method, the square bracket [] operator could be used for line [0] to retrieve the first character. The startsWith method can also take a regular expression or a string to match, as well as a starting index. Refer to the dart:core documentation for more information. For the purposes of parsing the presentation, the startsWith method is more readable.

Displaying the current slide

The slide is displayed via the showSlide method in slideShowApp.dart. To preview the current slide, the current index, which is stored in the field currentSlideIndex, is used to retrieve the desired slide object and the Div rendering method is called:

  showSlide(int slideNumber) {
    if (currentSlideShow.slides.length == 0) return;

    slideScreen.style.visibility = "hidden";
    slideScreen
  ..nodes.clear()
  ..nodes.add(currentSlideShow.slides[slideNumber].getSlideContents());

    rangeSlidePos.value = slideNumber.toString();
    slideScreen.style.visibility = "visible";
  }

The slideScreen is a DivElement that is then updated off screen by setting the visibility style property to hidden. The existing content of the DivElement is emptied out by calling nodes.clear() and the slide content is added with nodes.add. The range slider position is set, and finally, the DivElement is set to visible again.

Navigating the presentation

A button set with the familiar first, previous, next, and last slide allows the user to jump around the preview of the presentation. This is carried out by having an index built into the list of slides and stored in the field slide in the SlideShowApp class.

Handling the button key presses

The navigation buttons require being set up in an identical pattern in the constructor of the SlideShowApp object. First, get an object reference using id, which is the id attribute of the element, and then attach a handler to the click event. Rather than repeat this code, a simple function can handle the process:

setButton(String id, Function clickHandler) {
  ButtonInputElement btn = querySelector(id);
  btn.onClick.listen(clickHandler);
}

Because Function is a type in Dart, functions can be passed around easily as a parameter. Let us take a look at the button that takes us to the first slide:

setButton("#btnFirst", startSlideShow);

void startSlideShow(MouseEvent event) {
  showFirstSlide();
}

void showFirstSlide() {
  showSlide(0);
}

The event handlers do not directly change the slide; these are carried out by other methods that may be triggered by other inputs such as the keyboard.

Using the Function type

The SlideShowApp constructor makes use of this feature:

Function qs = querySelector;
var controls = qs("#controls");

I find the querySelector method a little long to type (though it is descriptive of what it does). With Function being comprised of types, we can easily create a shorthand version.

The constructor spends much of its time selecting and assigning the HTML elements to member fields of the class. One of the advantages of this approach is that the DOM of the page is queried only once, and the reference is stored and reused. This is good for performance of the application as, once the application is running, querying the DOM may take much longer.

Staying within the bounds

Using the min and max functions from the dart:math package, the index can be kept in the range of the current list:

void showLastSlide() { 
  currentSlideIndex = max(0, currentSlideShow.slides.length - 1); 
  showSlide(currentSlideIndex); 
} 
void showNextSlide() {
  currentSlideIndex = min(currentSlideShow.slides.length - 1, ++currentSlideIndex);
  showSlide(currentSlideIndex);
}

These convenient functions can save a great deal of if and else if comparisons and help make the code a good degree more readable.

Using the slider control

The slider control is another new control in the HTML5 standard. This will allow the user to scroll though the slides in the presentation.

Using the slider control

This control is a personal favorite of mine as it is so visual and can be used to give very interactive feedback to the user. It seemed to be a huge omission from the original form controls in the early generation of web browsers. Even with clear, widely accepted features, HTML specifications can take a long time to clear committees and make it into everyday browsers!

<input type="range" id="rngSlides" value="0"/>

The control has an onChange event that is given a listener in the SlideShowApp constructor:

rangeSlidepos.onChange.listen(moveToSlide);rangeSlidepos.onChange.listen(moveToSlide);

The control provides its data via a simple string value, which can be converted to an integer via the int.parse method to be used as an index in the presentation's slide list:

  void moveToSlide(Event event) {
    currentSlideIndex = int.parse(rangeSlidePos.value);
    showSlide(currentSlideIndex);
  }

The slider control must be kept in synchronization with any other change in its slide display, use of navigation, or change in number of slides. For example, the user may use the slider to reach the general area of the presentation, and then adjust with the Previous and Next buttons:

void updateRangeControl() {
  rangeSlidepos 
  ..min = "0" 
  ..max = (currentSlideShow.slides.length - 1).toString(); 
}

This method is called when the number of slides is changed, and as with working with most HTML elements, the values to be set need to be converted to strings.

Responding to keyboard events

Using the keyboard, particularly the arrow (cursor) keys, is a natural way to look through the slides in a presentation, even in the preview mode. This is carried out in the SlideShowApp constructor.

Note

In Dart web applications, the dart:html package allows direct access to the globalwindow object from any class or function.

The Textarea used to input the presentation source will also respond to the arrow keys, so there will need to be a check to see if it is currently being used. The property activeElement on the document will give a reference to the control with focus. This reference can be compared to the Textarea, which is stored in the presEditor field, so a decision can be made on whether to act on the keypress or not.

Keyboard events, like other events, can be listened to by using a stream event listener. The listener function is an anonymous function (the definition omits a name) that takes the KeyboardEvent as its only parameter:

window.onKeyUp.listen((KeyboardEvent e) {
  if (presEditor != document.activeElement){ 
  if (e.keyCode == 39) 
  showNextSlide(); 
  else if (e.keyCode == 37) 
  showPrevSlide(); 
  else if (e.keyCode == 38) 
  showFirstSlide(); 
  else if (e.keyCode == 40) 
  showLastSlide(); 
  }
});

Note

It is a reasonable question to ask how to get the keyboard key codes required to write the switching code. One good tool is the W3C's Key and Character Codes page at http://www.w3.org/2002/09/tests/keys.html. Although this documentation is helpful with this question, it can often be faster to write the handler and print out the event that is passed in.

Showing the key help

Rather than testing the user's memory, there will be a handy reference to the keyboard shortcuts.

Showing the key help

This is a simple Div element that is shown and then hidden when the key (remember to press Shift, too!) is pressed again by toggling the visibility style from visible to hidden.

Listening twice to event streams

The event system in Dart is implemented as a stream. One of the advantages of this is that an event can easily have more than one entity listening to the class.

This is useful, for example, in a web application where some keyboard presses are valid in one context but not in another. The listen method is an add operation (accumulative) so that the key press for help can be implemented separately. This allows a modular approach, which helps reuse, as the handlers can be specialized and added as required:

window.onKeyUp.listen((KeyboardEvent e) {
  print(e);

  //Check the editor does not have focus.
  if (presEditor != document.activeElement) {
    DivElement helpBox = qs("#helpKeyboardShortcuts");
    if (e.keyCode == 191) {
      if (helpBox.style.visibility == "visible") {
        helpBox.style.visibility = "hidden";
      } else {
        helpBox.style.visibility = "visible";
      }
    }
  }
});

In a game, for example, a common set of event handling may apply to the title and introduction screen, and the actual in-game screen can contain additional event handling as a superset. This can be implemented by adding and removing handlers to the relevant event stream.

主站蜘蛛池模板: 西青区| 通化市| 富宁县| 赣州市| 定兴县| 衡山县| 三穗县| 乐都县| 泽州县| 襄城县| 白银市| 青岛市| 襄樊市| 阜康市| 崇文区| 栾城县| 望都县| 丁青县| 宜良县| 红安县| 泉州市| 资中县| 分宜县| 山东| 呈贡县| 宣恩县| 长海县| 富平县| 文登市| 广丰县| 耿马| 牟定县| 晋江市| 聂拉木县| 卢氏县| 垣曲县| 类乌齐县| 含山县| 台江县| 谢通门县| 江都市|