- Fixed MR-84
[castle.git] / MonoRail / Castle.MonoRail.Framework / Helpers / PaginationHelper.cs
blobc6bb4f78739ba8a6e90dff5b5958afb7aa12ad38
1 // Copyright 2004-2007 Castle Project - http://www.castleproject.org/
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
15 namespace Castle.MonoRail.Framework.Helpers
17 using System;
18 using System.Collections;
19 using System.Collections.Generic;
20 using System.Collections.Specialized;
22 /// <summary>
23 /// Used as callback handler to obtain the items
24 /// to be displayed.
25 /// </summary>
26 public delegate IList DataObtentionDelegate();
28 /// <summary>
29 /// This helper allows you to easily paginate through a data source
30 /// -- anything that implements <see cref="IList"/>.
31 /// </summary>
32 ///
33 /// <remarks>
34 /// With the pagination you expose a <see cref="Page"/> instance to your view,
35 /// that can be used to created a very detailed page navigator.
36 ///
37 /// <para>
38 /// You can use up to three approaches to pagination:
39 /// </para>
40 ///
41 /// <list type="bullet">
42 /// <item>
43 /// <term>CreatePagination</term>
44 /// <description>Uses a whole data set and creates a <see cref="Page"/> with a slice of it. </description>
45 /// </item>
46 /// <item>
47 /// <term>CreateCachedPagination</term>
48 /// <description>Caches the dataset and creates a <see cref="Page"/> with a slice.
49 /// As the cache is shared, you must be very careful on creating a cache key that uniquely represents the
50 /// cached dataset.
51 /// </description>
52 /// </item>
53 /// <item>
54 /// <term>CreateCustomPage</term>
55 /// <description>
56 /// In this case, you are handling the slicing. The <see cref="Page"/> is created with your
57 /// actual dataset and information about total. It calculates the other information based on that.
58 /// </description>
59 /// </item>
60 /// </list>
61 ///
62 /// <para>
63 /// Performance wise, the best choice is the <see cref="CreateCustomPage(IList,int,int,int)"/>
64 /// </para>
65 /// </remarks>
66 public class PaginationHelper : AbstractHelper
68 /// <summary>
69 /// The parameter key that the helper looks for on the request
70 /// </summary>
71 public const string PageParameterName = "page";
73 #region CreatePageLink
75 /// <summary>
76 /// Creates a link to navigate to a specific page
77 /// </summary>
78 /// <param name="page">Page index</param>
79 /// <param name="text">Link text</param>
80 /// <returns>An anchor tag</returns>
81 public String CreatePageLink(int page, String text)
83 return CreatePageLink(page, text, null, null);
86 /// <summary>
87 /// Creates a link to navigate to a specific page
88 /// </summary>
89 /// <param name="page">Page index</param>
90 /// <param name="text">Link text</param>
91 /// <param name="htmlAttributes">Attributes for the anchor tag</param>
92 /// <returns>An anchor tag</returns>
93 public String CreatePageLink(int page, String text, IDictionary htmlAttributes)
95 return CreatePageLink(page, text, htmlAttributes, null);
98 /// <summary>
99 /// Creates a link to navigate to a specific page
100 /// </summary>
101 /// <param name="page">Page index</param>
102 /// <param name="text">Link text</param>
103 /// <param name="htmlAttributes">Attributes for the anchor tag</param>
104 /// <param name="queryStringParams">Query string entries for the link</param>
105 /// <returns>An anchor tag</returns>
106 public String CreatePageLink(int page, String text, IDictionary htmlAttributes, IDictionary queryStringParams)
108 String filePath = CurrentContext.Request.FilePath;
110 if (queryStringParams == null)
112 queryStringParams = new Hashtable();
114 else if (queryStringParams.IsReadOnly || queryStringParams.IsFixedSize)
116 queryStringParams = new Hashtable(queryStringParams);
119 queryStringParams[PageParameterName] = page.ToString();
121 return String.Format("<a href=\"{0}?{1}\" {2}>{3}</a>",
122 filePath, BuildQueryString(queryStringParams), GetAttributes(htmlAttributes), text);
125 /// <summary>
126 /// Creates a link to navigate to a specific page
127 /// </summary>
128 /// <param name="page">Page index</param>
129 /// <param name="text">Link text</param>
130 /// <param name="htmlAttributes">Attributes for the anchor tag</param>
131 /// <returns>An anchor tag</returns>
132 public String CreatePageLinkWithCurrentQueryString(int page, String text, IDictionary htmlAttributes)
134 NameValueCollection queryStringParams = Controller.Request.QueryString;
135 IDictionary dictionary = null;
136 if (queryStringParams != null && queryStringParams.Count > 0)
138 dictionary = new Hashtable(queryStringParams.Count);
139 foreach(string key in queryStringParams.Keys)
141 if (key != null)
143 dictionary[key] = queryStringParams.GetValues(key);
147 return CreatePageLink(page, text, htmlAttributes, dictionary);
150 #endregion
152 #region CreatePagination
154 /// <summary>
155 /// Creates a <see cref="Page"/> which is a sliced view of
156 /// the data source
157 /// </summary>
158 /// <param name="controller">the current controller</param>
159 /// <param name="datasource">Data source to be used as target of the pagination</param>
160 /// <param name="pageSize">Page size</param>
161 /// <returns>A <see cref="Page"/> instance</returns>
162 public static IPaginatedPage CreatePagination(Controller controller, IList datasource, int pageSize)
164 return CreatePagination(datasource, pageSize, GetCurrentPageFromRequest(controller));
167 /// <summary>
168 /// Creates a <see cref="Page"/> which is a sliced view of
169 /// the data source
170 /// </summary>
171 /// <param name="datasource">Data source to be used as target of the pagination</param>
172 /// <param name="pageSize">Page size</param>
173 /// <param name="currentPage">current page index (1 based)</param>
174 /// <returns>A <see cref="Page"/> instance</returns>
175 public static IPaginatedPage CreatePagination(IList datasource, int pageSize, int currentPage)
177 if (currentPage <= 0) currentPage = 1;
179 return new Page(datasource, currentPage, pageSize);
182 #endregion
184 #region CreatePagination<T>
186 /// <summary>
187 /// Creates a <see cref="Page"/> which is a sliced view of
188 /// the data source
189 /// </summary>
190 /// <param name="controller">the current controller</param>
191 /// <param name="datasource">Data source to be used as target of the pagination</param>
192 /// <param name="pageSize">Page size</param>
193 /// <returns>A <see cref="Page"/> instance</returns>
194 public static IPaginatedPage CreatePagination<T>(Controller controller, ICollection<T> datasource, int pageSize)
196 return CreatePagination<T>(datasource, pageSize, GetCurrentPageFromRequest(controller));
199 /// <summary>
200 /// Creates a <see cref="Page"/> which is a sliced view of
201 /// the data source
202 /// </summary>
203 /// <param name="datasource">Data source to be used as target of the pagination</param>
204 /// <param name="pageSize">Page size</param>
205 /// <param name="currentPage">current page index (1 based)</param>
206 /// <returns>A <see cref="Page"/> instance</returns>
207 public static IPaginatedPage CreatePagination<T>(ICollection<T> datasource, int pageSize, int currentPage)
209 if (currentPage <= 0) currentPage = 1;
211 return new GenericPage<T>(datasource, currentPage, pageSize);
214 #endregion
216 #region CreateCachedPagination
218 /// <summary>
219 /// Creates a <see cref="Page"/> which is a sliced view of
220 /// the data source. This method first looks for the datasource
221 /// in the <see cref="System.Web.Caching.Cache"/> and if not found,
222 /// it invokes the <c>dataObtentionCallback</c> and caches the result
223 /// using the specifed <c>cacheKey</c>
224 /// </summary>
226 /// <remarks>
227 /// CreateCachedPagination is quite dangerous exactly because the cache is
228 /// shared. If the results vary per logged user, then the programmer must
229 /// rather pay a lot of attention when generating the cache key.
230 ///
231 /// It is preferable to have caching happen at a lower level of the stack, for example the NHibernate query cache.
232 /// </remarks>
234 /// <param name="controller">the current controller</param>
235 /// <param name="cacheKey">Cache key used to query/store the datasource</param>
236 /// <param name="pageSize">Page size</param>
237 /// <param name="dataObtentionCallback">Callback to be used to populate the cache</param>
238 /// <returns>A <see cref="Page"/> instance</returns>
239 public static IPaginatedPage CreateCachedPagination(Controller controller, String cacheKey, int pageSize,
240 DataObtentionDelegate dataObtentionCallback)
242 IList datasource = (IList) GetCache(controller).Get(cacheKey);
244 if (datasource == null)
246 datasource = dataObtentionCallback();
248 GetCache(controller).Store(cacheKey, datasource);
251 return CreatePagination(controller, datasource, pageSize);
254 #endregion
256 #region CreateCustomPage
258 /// <summary>
259 /// Creates a <see cref="Page"/> which is a sliced view of
260 /// the data source
261 /// <para>
262 /// Assumes that the slicing is managed by the caller.
263 /// </para>
264 /// </summary>
265 /// <param name="controller">the current controller</param>
266 /// <param name="datasource">Data source to be used as target of the pagination</param>
267 /// <param name="pageSize">Page size</param>
268 /// <param name="total">The total of items in the datasource</param>
269 /// <returns>A <see cref="Page"/> instance</returns>
270 public static IPaginatedPage CreateCustomPage(Controller controller, IList datasource, int pageSize, int total)
272 return CreateCustomPage(datasource, pageSize, GetCurrentPageFromRequest(controller), total);
275 /// <summary>
276 /// Creates a <see cref="Page"/> which is a sliced view of
277 /// the data source
278 /// <para>
279 /// Assumes that the slicing is managed by the caller.
280 /// </para>
281 /// </summary>
282 /// <param name="datasource">Data source to be used as target of the pagination</param>
283 /// <param name="pageSize">Page size</param>
284 /// <param name="total">The total of items in the datasource</param>
285 /// <param name="currentPage">The current page index (1 based).</param>
286 /// <returns></returns>
287 public static IPaginatedPage CreateCustomPage(IList datasource, int pageSize, int currentPage, int total)
289 if (currentPage <= 0) currentPage = 1;
291 return new Page(datasource, currentPage, pageSize, total);
294 /// <summary>
295 /// Creates a <see cref="Page"/> which is a sliced view of
296 /// the data source
297 /// <para>
298 /// Assumes that the slicing is managed by the caller.
299 /// </para>
300 /// </summary>
301 /// <param name="datasource">Data source to be used as target of the pagination</param>
302 /// <param name="pageSize">Page size</param>
303 /// <param name="total">The total of items in the datasource</param>
304 /// <param name="currentPage">The current page index (1 based).</param>
305 /// <returns></returns>
306 public static IPaginatedPage CreateCustomPage<T>(IList<T> datasource, int pageSize, int currentPage, int total)
308 if (currentPage <= 0) currentPage = 1;
310 return new GenericCustomPage<T>(datasource, currentPage, pageSize, total);
313 #endregion
315 private static ICacheProvider GetCache(Controller controller)
317 return controller.Context.Cache;
320 private static int GetCurrentPageFromRequest(Controller controller)
322 String currentPage = GetParameter(controller, PageParameterName);
324 int curPage = 1;
326 if (currentPage != null && currentPage != String.Empty)
328 curPage = Int32.Parse(currentPage);
331 return curPage <= 0 ? 1 : curPage;
334 private static string GetParameter(Controller controller, string parameterName)
336 return controller.Context.Request.Params[parameterName];
340 /// <summary>
341 /// Represents the sliced data and offers
342 /// a few read only properties to create a pagination bar.
343 /// </summary>
344 [Serializable]
345 public class Page : AbstractPage
347 private readonly IList slice = new ArrayList();
348 private int startIndex;
349 private int endIndex;
351 /// <summary>
352 /// Initializes a new instance of the <see cref="Page"/> class.
353 /// </summary>
354 /// <param name="curPage">The desired page index</param>
355 /// <param name="pageSize">The desired page size</param>
356 /// <param name="total">The total of items in the data source.</param>
357 protected Page(int curPage, int pageSize, int total)
359 startIndex = (pageSize * curPage) - pageSize;
360 endIndex = Math.Min(startIndex + pageSize, total);
362 CalculatePaginationInfo(startIndex, endIndex, total, pageSize, curPage);
365 /// <summary>
366 /// Constructs a Page using the specified parameters
367 /// </summary>
368 /// <param name="list">The whole set</param>
369 /// <param name="curPage">The desired page index</param>
370 /// <param name="pageSize">The desired page size</param>
371 public Page(IList list, int curPage, int pageSize) : this(curPage, pageSize, list.Count)
373 CreateSlicedCollection(startIndex, endIndex, list);
376 /// <summary>
377 /// Initializes a new instance of the <see cref="Page"/> class.
378 /// </summary>
379 /// <param name="slice">The sliced list.</param>
380 /// <param name="curPage">The desired page index</param>
381 /// <param name="pageSize">The desired page size</param>
382 /// <param name="total">The total of items (not in the list, but on the original source).</param>
383 public Page(IList slice, int curPage, int pageSize, int total) : this(curPage, pageSize, total)
385 this.slice = slice;
388 /// <summary>
389 /// Populates the sliced view of the whole set
390 /// </summary>
391 /// <param name="startIndex">Index to start to</param>
392 /// <param name="endIndex">Last index</param>
393 /// <param name="list">Source set</param>
394 private void CreateSlicedCollection(int startIndex, int endIndex, IList list)
396 for(int index = startIndex; index < endIndex; index++)
398 slice.Add(list[index]);
402 /// <summary>
403 /// Creates an enumerator for the
404 /// sliced set
405 /// </summary>
406 /// <returns>An enumerator instance</returns>
407 public override IEnumerator GetEnumerator()
409 return slice.GetEnumerator();
413 /// <summary>
414 /// Represents the sliced data and offers
415 /// a few read only properties to create a pagination bar.
416 /// </summary>
417 [Serializable]
418 public class GenericPage<T> : AbstractPage
420 private readonly int sliceStart, sliceEnd;
421 private readonly ICollection<T> sourceList;
423 /// <summary>
424 /// Initializes a new instance of the <see cref="GenericPage&lt;T&gt;"/> class.
425 /// </summary>
426 /// <param name="list">The list.</param>
427 /// <param name="curPage">The cur page.</param>
428 /// <param name="pageSize">Size of the page.</param>
429 public GenericPage(ICollection<T> list, int curPage, int pageSize)
431 // Calculate slice indexes
432 int startIndex = sliceStart = (pageSize * curPage) - pageSize;
433 int endIndex = sliceEnd = Math.Min(startIndex + pageSize, list.Count);
435 sourceList = list;
437 CalculatePaginationInfo(startIndex, endIndex, list.Count, pageSize, curPage);
440 /// <summary>
441 /// Returns a enumerator for the contents
442 /// of this page only (not the whole set)
443 /// </summary>
444 /// <returns>Enumerator instance</returns>
445 public override IEnumerator GetEnumerator()
447 if (sourceList is IList<T>)
449 IList<T> list = (IList<T>) sourceList;
450 for(int i = sliceStart; i < sliceEnd; i++)
452 yield return list[i];
455 else if (sourceList is IList)
457 IList list = (IList) sourceList;
458 for(int i = sliceStart; i < sliceEnd; i++)
460 yield return list[i];
463 else
465 IEnumerator en = sourceList.GetEnumerator();
466 for(int i = 0; i < sliceEnd; i++)
468 if (!en.MoveNext())
469 yield break;
471 if (i < sliceStart)
472 continue;
474 yield return en.Current;
480 /// <summary>
481 /// Represents the sliced data and offers
482 /// a few read only properties to create a pagination bar.
483 /// </summary>
484 [Serializable]
485 public class GenericCustomPage<T> : AbstractPage, IEnumerable<T>
487 private readonly IList<T> sourceList;
489 /// <summary>
490 /// Initializes a new instance of the <see cref="GenericCustomPage&lt;T&gt;"/> class.
491 /// </summary>
492 /// <param name="list">The list.</param>
493 /// <param name="curPage">The cur page.</param>
494 /// <param name="pageSize">Size of the page.</param>
495 /// <param name="total">The total.</param>
496 public GenericCustomPage(IList<T> list, int curPage, int pageSize, int total)
498 int startIndex = (pageSize * curPage) - pageSize;
499 int endIndex = Math.Min(startIndex + pageSize, total);
501 sourceList = list;
503 CalculatePaginationInfo(startIndex, endIndex, total, pageSize, curPage);
506 /// <summary>
507 /// Returns a enumerator for the contents
508 /// of this page only (not the whole set)
509 /// </summary>
510 /// <returns>Enumerator instance</returns>
511 public override IEnumerator GetEnumerator()
513 for(int i = 0; i < sourceList.Count; i++)
515 yield return sourceList[i];
519 /// <summary>
520 /// Returns an enumerator that iterates through the collection.
521 /// </summary>
522 /// <returns>
523 /// A <see cref="T:System.Collections.Generic.IEnumerator`1"></see> that can be used to iterate through the collection.
524 /// </returns>
525 IEnumerator<T> IEnumerable<T>.GetEnumerator()
527 for(int i = 0; i < sourceList.Count; i++)
529 yield return sourceList[i];
534 /// <summary>
535 /// Abstract implementation of <see cref="IPaginatedPage"/>
536 /// which performs the standard calculations on
537 /// <see cref="CalculatePaginationInfo"/>
538 /// </summary>
539 [Serializable]
540 public abstract class AbstractPage : IPaginatedPage
542 private int firstItem, lastItem, totalItems, pageSize;
543 private int previousIndex, nextIndex, lastIndex, curIndex;
544 private bool hasPrev, hasNext, hasFirst, hasLast;
546 /// <summary>
547 /// Calculate the values of all properties
548 /// based on the specified parameters
549 /// </summary>
550 /// <param name="startIndex">Start index</param>
551 /// <param name="endIndex">Last index</param>
552 /// <param name="count">Total of elements</param>
553 /// <param name="pageSize">Page size</param>
554 /// <param name="curPage">This page index</param>
555 protected void CalculatePaginationInfo(int startIndex, int endIndex, int count, int pageSize, int curPage)
557 firstItem = count != 0 ? startIndex + 1 : 0;
558 lastItem = endIndex;
559 totalItems = count;
561 hasPrev = startIndex != 0;
562 hasNext = count == -1 || (startIndex + pageSize) < count;
563 hasFirst = curPage != 1;
564 hasLast = count > curPage * pageSize;
566 curIndex = curPage;
567 previousIndex = curPage - 1;
568 nextIndex = curPage + 1;
569 lastIndex = count == -1 ? -1 : count / pageSize;
571 this.pageSize = pageSize;
573 if (count != -1 && count / (float) pageSize > lastIndex)
575 lastIndex++;
579 /// <summary>
580 /// The first index
581 /// </summary>
582 public int FirstIndex
584 get { return 1; }
587 /// <summary>
588 /// The index this page represents
589 /// </summary>
590 public int CurrentIndex
592 get { return curIndex; }
595 /// <summary>
596 /// The last index available on the set
597 /// </summary>
598 public int LastIndex
600 get { return lastIndex; }
603 /// <summary>
604 /// The previous index (from this page)
605 /// </summary>
606 public int PreviousIndex
608 get { return previousIndex; }
611 /// <summary>
612 /// The next index (from this page)
613 /// </summary>
614 public int NextIndex
616 get { return nextIndex; }
619 /// <summary>
620 /// The first element (index + 1)
621 /// </summary>
622 public int FirstItem
624 get { return firstItem; }
627 /// <summary>
628 /// The last element in the page (count)
629 /// </summary>
630 public int LastItem
632 get { return lastItem; }
635 /// <summary>
636 /// The count of all elements on the set
637 /// </summary>
638 public int TotalItems
640 get { return totalItems; }
643 /// <summary>
644 /// Gets the size of the page.
645 /// </summary>
646 /// <value>The size of the page.</value>
647 public int PageSize
649 get { return pageSize; }
652 /// <summary>
653 /// Returns true if a previous page
654 /// is accessible from this page
655 /// </summary>
656 public bool HasPrevious
658 get { return hasPrev; }
661 /// <summary>
662 /// Returns true if a next page is
663 /// accessible from this page
664 /// </summary>
665 public bool HasNext
667 get { return hasNext; }
670 /// <summary>
671 /// Returns true if a first page
672 /// exists
673 /// </summary>
674 public bool HasFirst
676 get { return hasFirst; }
679 /// <summary>
680 /// Returns true if a last page
681 /// exists
682 /// </summary>
683 public bool HasLast
685 get { return hasLast; }
688 /// <summary>
689 /// Checks whether the specified page exists.
690 /// Useful for Google-like pagination.
691 /// </summary>
692 /// <param name="pageNumber">The page number</param>
693 public bool HasPage(int pageNumber)
695 return pageNumber <= LastIndex;
698 /// <summary>
699 /// Returns a enumerator for the contents
700 /// of this page only (not the whole set)
701 /// </summary>
702 /// <returns>Enumerator instance</returns>
703 public abstract IEnumerator GetEnumerator();