/ext-4.1.0_b3/examples/MVC/article.mdown
Unknown | 433 lines | 331 code | 102 blank | 0 comment | 0 complexity | d5a4d7f4e71f72e937520a1f7ec3d497 MD5 | raw file
1In the previous article we explored what it would mean to architect a Pandora-style application inside of Ext JS. We took a look at starting with the Model-View-Controller architecture and how to apply it to a relatively complex UI applications that had multiple views and multiple models. In this article we're going to explore how to move beyond just architecting it visually, and how to design and code the controllers and the models, and how it all wires up, starting with Ext.application and the Viewport class.
2
3Let's actually jump in and start writing the application.
4
5## Defining our application
6In Ext 3, the entry point into your application was the Ext.onReady method, leaving it up to the developer to come up with an application architecture. In Ext 4 we have an introduced an MVC-like pattern. Using this helps you follow best practices when creating your applications.
7
8The entry point into an application written using the new MVC package will require you to use the ***Ext.application*** method. This method will create an ***Ext.app.Application*** instance for you and will fire the ***launch*** method as soon as the page is ready. This essentially replaced the need to use ***Ext.onReady*** while adding new functionality like automatically creating a viewport and setting up your namespace.
9
10***app/Application.js***
11
12 Ext.application({
13 name: 'Panda',
14 autoCreateViewport: true,
15 launch: function() {
16 // This is fired as soon as the page is ready
17 }
18 });
19
20The *name* configuration causes a new namespace to be created. All our views, models, stores and controllers will live in this namespace. By setting *autoCreateViewport* to true, the framework will by convention include the **app/view/Viewport.js** file. In this file a class should be defined with the name **Panda.view.Viewport**, matching the namespace that was specified by the ***name*** configuration on your application.
21
22## The Viewport class
23When we looked at what views were needed for our UI we were very focused on the individual parts. The Viewport of an application acts as the glue for these individual parts. It loads the required views and defines the configuration needed to achieve your app's overall layout. We have found that progressively defining our views and adding them to the viewport is the fastest way to create the base structure of your UI.
24
25It is important during this process to focus on scaffolding your views and not on the individual views themselves. Its almost like sculpting. We start by creating the very rough shapes of our views, and add more detail to them later.
26
27## Creating the building blocks
28Leveraging the preparations we already did in the previous article, we are able to define many of the views at once.
29
30
31
32***app/view/NewStation.js***
33
34 Ext.define('Panda.view.NewStation', {
35 extend: 'Ext.form.field.ComboBox',
36 alias: 'widget.newstation',
37 store: 'SearchResults',
38 ... more configuration ...
39 });
40
41***app/view/SongControls.js***
42
43 Ext.define('Panda.view.SongControls', {
44 extend: 'Ext.Container',
45 alias: 'widget.songcontrols',
46 ... more configuration ...
47 });
48
49***app/view/StationsList***
50
51 Ext.define('Panda.view.StationsList', {
52 extend: 'Ext.grid.Panel',
53 alias: 'widget.stationslist',
54 store: 'Stations',
55 ... more configuration ...
56 });
57
58***app/view/RecentlyPlayedScroller.js***
59
60 Ext.define('Panda.view.RecentlyPlayedScroller', {
61 extend: 'Ext.view.View',
62 alias: 'widget.recentlyplayedscroller',
63 itemTpl: '<div>{name}</div>',
64 store: 'RecentSongs',
65 ... more configuration ...
66 });
67
68***app/view/SongInfo.js***
69
70 Ext.define('Panda.view.SongInfo', {
71 extend: 'Ext.panel.Panel',
72 alias: 'widget.songinfo',
73 tpl: '<h1>About {artist}</h1><p>{description}</p>',
74 ... more configuration ...
75 });
76
77We have left out some of the configuration here since component configurations are not in the scope of this article.
78
79
80In the above configurations you'll notice that we have three stores configured. These map to the store names prepared in the previous article. At this point we'll need to go ahead and create our stores.
81
82## The models and stores
83Often it is useful to start with static json files containing mock data to act as our server side. Later we can use these static files as a reference when actually implementing a dynamic server side.
84
85For our app we had decided to use two models, Station and Song. We also need three stores using these two models that will be bound to our data components. Each store will load its data from the server-side. The mock data files would look something like the following.
86
87### Static data
88***data/songs.json***
89
90 {
91 'success': true,
92 'results': [
93 {
94 'name': 'Blues At Sunrise (Live)',
95 'artist': 'Stevie Ray Vaughan',
96 'album': 'Blues At Sunrise',
97 'description': 'Description for Stevie',
98 'played_date': '1',
99 'station': 1
100 },
101 ...
102 ]
103 }
104
105***data/stations.json***
106
107 {
108 'success': true,
109 'results': [
110 {'id': 1, 'played_date': 4, 'name': 'Led Zeppelin'},
111 {'id': 2, 'played_date': 3, 'name': 'The Rolling Stones'},
112 {'id': 3, 'played_date': 2, 'name': 'Daft Punk'}
113 ]
114 }
115
116***data/searchresults.json***
117
118 {
119 'success': true,
120 'results': [
121 {'id': 1, 'name': 'Led Zeppelin'},
122 {'id': 2, 'name': 'The Rolling Stones'},
123 {'id': 3, 'name': 'Daft Punk'},
124 {'id': 4, 'name': 'John Mayer'},
125 {'id': 5, 'name': 'Pete Philly & Perquisite'},
126 {'id': 6, 'name': 'Black Star'},
127 {'id': 7, 'name': 'Macy Gray'}
128 ]
129 }
130
131### Models
132Models in Ext 4 are very similar to Records we had in Ext 3. One key difference is that you can now specify a proxy on your model, as well as validations and associations. The Song model for our application in Ext 4 would look like this.
133
134***app/model/Song.js***
135
136 Ext.define('Panda.model.Song', {
137 extend: 'Ext.data.Model',
138 fields: ['id', 'name', 'artist', 'album', 'played_date', 'station'],
139
140 proxy: {
141 type: 'ajax',
142 url: 'data/recentsongs.json',
143 reader: {
144 type: 'json',
145 root: 'results'
146 }
147 }
148 });
149
150As you can see we have defined the proxy on our model. It is generally good practice to do this as it allows you to load and save instances of this model without needing a store. Also, when multiple stores use this same model, we don't have to redefine our proxy on each one of them.
151
152Lets go ahead and also define our Station model.
153
154***app/model/Station.js***
155
156 Ext.define('Panda.model.Station', {
157 extend: 'Ext.data.Model',
158 fields: ['id', 'name', 'played_date'],
159
160 proxy: {
161 type: 'ajax',
162 url: 'data/stations.json',
163 reader: {
164 type: 'json',
165 root: 'results'
166 }
167 }
168 });
169
170### Stores
171In Ext 4, multiple stores can use the same data model, even if the stores will load their data from different sources. In our example, the Station model will be used by both the SearchResults and the Stations store, both loading the data from a different location. One returns search results, the other returns the user's favorite stations. To achieve this, one of our stores will need to override the proxy defined on the model.
172
173***app/store/SearchResults.js***
174
175 Ext.define('Panda.store.SearchResults', {
176 extend: 'Ext.data.Store',
177 requires: 'Panda.model.Station',
178 model: 'Panda.model.Station',
179
180 // Overriding the model's default proxy
181 proxy: {
182 type: 'ajax',
183 url: 'data/searchresults.json',
184 reader: {
185 type: 'json',
186 root: 'results'
187 }
188 }
189 });
190
191***app/store/Stations.js***
192
193 Ext.define('Panda.store.Stations', {
194 extend: 'Ext.data.Store',
195 requires: 'Panda.model.Station',
196 model: 'Panda.model.Station'
197 });
198
199In the ***SearchResults*** store definition we have overridden the proxy defined on the ***Station*** model by providing a different proxy configuration. The store's proxy used when calling the store's *load* method instead of the proxy defined on the model itself.
200
201Note that you could implement your server-side to have one API for retrieving both search results and the user's favorite stations in which case both stores could use the default proxy defined on the model, only passing different parameters to the request when loading the stores.
202
203Lastly let's create the RecentSongs store.
204
205***app/store/RecentSongs.js***
206
207 Ext.define('Panda.store.RecentSongs', {
208 extend: 'Ext.data.Store',
209 model: 'Panda.model.Song',
210
211 // Make sure to require your model if you are
212 // not using Ext 4.0.5
213 requires: 'Panda.model.Song'
214 });
215
216Note that in the current version of Ext the 'model' property on a store doesn't automatically create a dependency, which is why we have to specify *requires* in order to be able to dynamically load the model.
217
218Lastly, for convention, we always try to pluralize the store names, while keeping the model names singular.
219
220### Adding the stores and models to our application
221Now that we have defined our models and stores, its time to add them to our application. Let's revisit our Application.js file.
222
223***app/Application.js***
224
225 Ext.application({
226 ...
227 models: ['Station', 'Song'],
228 stores: ['Stations', 'RecentSongs', 'SearchResults']
229 ...
230 });
231
232Another advantage of using the new Ext 4 MVC package is that the Application will automatically load the stores and models defined in the ***stores*** and ***models*** configurations. Then it will create an instance for each store loaded, giving it a storeId equal to its name. This allows us to use the name of the store whenever we bind it to a data component like we did in our views, e.g. store: 'SearchResults'.
233
234## Applying the glue
235Now that we have our views, models and stores, it's time to glue them together. You start by adding the views one by one to your viewport. This will make it easier to debug any wrong view configurations. Let's go through the resulting viewport for the Panda app.
236
237 Ext.define('Panda.view.Viewport', {
238 extend: 'Ext.container.Viewport',
239
240Your Viewport class will usually want to extend *Ext.container.Viewport*. This will cause your app to take up all the available space in your browser window.
241
242 requires: [
243 'Panda.view.NewStation',
244 'Panda.view.SongControls',
245 'Panda.view.StationsList',
246 'Panda.view.RecentlyPlayedScroller',
247 'Panda.view.SongInfo'
248 ],
249
250We set up all the view dependencies in our viewport. This will allow us to use their xtypes, previously configured in our views using the ***alias*** property.
251
252 layout: 'fit',
253
254 initComponent: function() {
255 this.items = {
256 xtype: 'panel',
257 dockedItems: [{
258 dock: 'top',
259 xtype: 'toolbar',
260 height: 80,
261 items: [{
262 xtype: 'newstation',
263 width: 150
264 }, {
265 xtype: 'songcontrols',
266 height: 70,
267 flex: 1
268 }, {
269 xtype: 'component',
270 html: 'Panda<br>Internet Radio'
271 }]
272 }],
273 layout: {
274 type: 'hbox',
275 align: 'stretch'
276 },
277 items: [{
278 width: 250,
279 xtype: 'panel',
280 layout: {
281 type: 'vbox',
282 align: 'stretch'
283 },
284 items: [{
285 xtype: 'stationslist',
286 flex: 1
287 }, {
288 html: 'Ad',
289 height: 250,
290 xtype: 'panel'
291 }]
292 }, {
293 xtype: 'container',
294 flex: 1,
295 layout: {
296 type: 'vbox',
297 align: 'stretch'
298 },
299 items: [{
300 xtype: 'recentlyplayedscroller',
301 height: 250
302 }, {
303 xtype: 'songinfo',
304 flex: 1
305 }]
306 }]
307 };
308
309 this.callParent();
310 }
311 });
312
313Since Viewport extends Container, and Containers can't have docked items (yet), we have added a Panel as the single item of our viewport. We make this panel the same size as our viewport by defining a layout of **fit**.
314
315In terms of architecture, one of the most important things to note here is the fact that we have not defined layout specific configuration in the actual views. By not defining properties like *flex*, *width*, *height* in the views we can easily adjust the application's overall layout in one single place, adding to the maintainability and flexibility of our architecture.
316
317## Application logic
318In Ext 3, we often added our application's logic to the views themselves using handlers on buttons, binding listeners to subcomponents, and overriding methods on the views themselves when extending them. However, just like you shouldn't inline CSS styles in your HTML markup, it's preferred to separate the application's logic from the view definitions. In Ext 4's MVC package we provide Controllers. They are responsible for listening to events fired by the views and other controllers, and implement application logic to act on those events. There are several benefits to this.
319
320One benefit is that your application logic is not bound to instances of views. This means that we can destroy and instantiate our views as needed while the application logic can continue to keep processing other things, like synchronizing data.
321
322Additionally in Ext 3, you might have had many nested views, each adding layers of application logic. Moving this application logic to controllers centralizes this logic, making it easier to maintain and change.
323
324Finally, the Controller base class provides you with lots of functionality, making it easier to implement your application logic.
325
326## Creating our Controllers
327Now that we have the basic architecture for our UI, models and stores set up it's time to get in control of our application. We planned to have two controllers, Station and Song, so let's create the definitions for them.
328
329***app/controller/Station.js***
330
331 Ext.define('Panda.controller.Station', {
332 extend: 'Ext.app.Controller',
333 init: function() {
334 ...
335 },
336 ...
337 });
338
339***app/controller/Song.js***
340
341 Ext.define('Panda.controller.Song', {
342 extend: 'Ext.app.Controller',
343 init: function() {
344 ...
345 },
346 ...
347 });
348
349When including the controllers in your application, the framework will automatically load the controller and call the ***init*** method on it. Inside of the init method you should set up listeners for your view and application events. In larger applications you might want to load additional controllers at runtime. You can do this by using the ***getController*** method.
350
351 someAction: function() {
352 var controller = this.getController('AnotherController');
353
354 // Remember to call the init method manually
355 controller.init();
356 }
357
358When you do this, you have to remember to call the ***init*** method on the loaded controller manually.
359
360For the purposes of our example application, we'll let the framework load and initialize our controllers by adding them to the ***controllers*** array in our application definition.
361
362***app/Application.js***
363
364 Ext.application({
365 ...
366 controllers: ['Station', 'Song']
367 });
368
369### Setting up listeners
370Let's start controlling some parts of our UI by using the ***control** method inside of the controller's init function.
371
372***app/controller/Station.js***
373
374 ...
375 init: function() {
376 this.control({
377 'stationslist': {
378 selectionchange: this.onStationSelect
379 },
380 'newstation': {
381 select: this.onNewStationSelect
382 }
383 });
384 }
385 ...
386
387The ***control*** method is passed an object where the keys are component queries. In our example the component queries are just using the xtypes of our views. However, using these component queries you can target very specific parts of your UI. To learn more about advanced component queries you can refer to the [API docs](http://docs.sencha.com/ext-js/4-0/#/api/Ext.ComponentQuery).
388
389Each query is bound to a listener configuration. Inside each listener configuration the key is the event name we want to listen for. The events available are the ones provided by the component targeted by your query. In this case we use the ***selectionchange*** event provided by Grid (which is what our StationsList view extends from) and the ***select*** event provided by ComboBox (which our NewStation view extends from). To find out which events are available for a particular component you can look in the events section available for each component in the API docs.
390
391
392
393The value in the listener configuration is the function that gets executed whenever that event fires. The scope of this function is always the controller itself.
394
395Let's also set up some listeners in our Song controller.
396
397***app/controller/Song.js***
398
399 ...
400 init: function() {
401 this.control({
402 'recentlyplayedscroller': {
403 selectionchange: this.onSongSelect
404 }
405 });
406
407 this.application.on({
408 stationstart: this.onStationStart,
409 scope: this
410 });
411 }
412 ..
413
414In addition to listening for the ***selectionchange*** event on our RecentlyPlayedScroller view, we also set up a listener for an application event here. We do this by using the ***on*** method on the ***application*** instance. Each controller has access to the application instance using the ***this.application*** reference.
415
416Application events are extremely useful for events that many controllers in your application are interested in. Instead of listening for the same view event in each of these controllers, only one controller will listen for the view event and fire an application wide event that the others can listen for. This also allows controllers to communicate to one another without knowing about or depending on each other's existence.
417
418Our Song controller is interested in a new station being started, because it needs to update the song scroller and song info whenever this happens.
419
420Let's take a look at how the Station controller, which will be the one responsible for firing this ***stationstart*** application event, actually does this.
421
422***app/controller/Station.js***
423
424 ...
425 onStationSelect: function(selModel, selection) {
426 this.application.fireEvent('stationstart', selection[0]);
427 }
428 ...
429
430We simply get the single selected item provided by the ***selectionchange*** event and pass it as the single argument when firing the ***stationstart*** event.
431
432## Conclusion
433In this article we have looked at the basic techniques of architecting your application. Of course there is a lot to it, and in the next part of this series we will take a look at some more advanced controller techniques and continue wiring up our Panda app by implementing our controller actions and adding some more details to our views. Please leave us the questions you have about the things we have discussed so far, so that we can make sure to answer them in the next article.