Click here to Skip to main content
15,868,141 members
Articles / Programming Languages / C#

Generic Collections: Interfaces & Classes

Rate me:
Please Sign up or sign in to vote.
4.85/5 (9 votes)
9 Jun 2009CPOL6 min read 60.1K   389   38   2
An intermediate level article for those who need a reference for Generics

Introduction

The best place to start an examination of the generics namespaces is with interfaces, because they define the signature of what will ultimately be possible with the library’s concrete types. At the root of the Base Class Library’s generic collection interface hierarchy is the IEnumerable<t> interface. IEnumerable is a one-way interface. It allows data to be returned from the collection. The general concept of an enumerator is that of a type whose purpose is to advance through and read another collection’s contents. Enumerators do not provide write capabilities. This type can be viewed as a cursor that advances over each element in a collection, one at a time. These method(s) do not come with any implementation at all. A class inherits an interface by specifying the interface’s name, and the class must explicitly provide implementations of the interface’s methods before the CLR will consider the type definition to be valid. This is why the best place to start an examination of the generic namespaces is with interfaces, because they define the signature of what will ultimately be possible with the libraries concrete types. It is limited to supporting iteration over a collection of objects and nothing more. The general concept of an enumerator is that of a type whose purpose is to advance through and read another collection’s contents. Enumerators do not provide write capabilities. This type can be viewed as a cursor that advances over each individual element in a collection, one at a time. The IEnumerable<t /> represents a type whose contents can be enumerated, while IEnumerator<t> is the type performing the actual enumeration:

C#
using System;
using System.Collections;
namespace System.Collections.Generic
 {
  public interface IEnumerable<T> : IEnumerable
    {
        // methods

     IEnumerator<T> GetEnumerator();
     IEnumerator GetEnumerator(); // inherited from IEnumerable
     }

      public interface IEnumerator<T> : IDisposable, IEnumerator
       {
       // properties

        T Current { get; }
        object Current { get; }      // inherited from IEnumerator

       //  methods

          void Dispose();
          bool MoveNext();
          void Reset();
    }
}

Upon instantiation, an enumerator becomes dependent on a collection. IEnumerable<t> is a simple interface but is implemented in every generic collection class in the Framework (as well as arrays):

C#
using System;
using System.Collections.Generic;
public sealed class Program {
public static void Main()
        {
            IEnumerable<string> enumerable = new string[] { "A", "B", "C" };

            // Short-hand form:
            foreach (string s in enumerable)
                Console.WriteLine(s);

            // Long-hand form:
            IEnumerator<string> enumerator = enumerable.GetEnumerator();
            while (enumerator.MoveNext())
            {
                string s = enumerator.Current;
                Console.WriteLine(s);
            }
        }
   }

The output is:

C
A
B
C
A
B
C

ICollection<T>

The ICollection<T> interface is used to represent a simple collection of items, each of type T. For example, an ICollection<string> contains a collection of string references. This interface exposes the capability to access and modify the contents of a collection and to determine its length. It also derives from IEnumerable<T>, meaning that any implementations of ICollections<T> will also supply a GetEnumerator method that returns an enumerator, using which you may walk its contents.

C#
namespace System.Collections.Generic
{
  public interface ICollection<T> : IEnumerable<T>
    {
       // properties

       int Count { get; }
       bool IsReadOnly     {   get;   }

       // methods

        void Add(T item);
        void Clear();
        bool Contains(T  item);
        void CopyTo(T[ ]  array, int arrayIndex);
        IEnumerator<T> GetEnumerator();
        IEnumerator GetEnumerator();
        bool Remove(T  item);
      }
}

The Add method adds an item at an unspecified location of the collection, and Remove removes (first occurrence of the) specified item from the list. Remove returns true if successfully removed from the item or false if otherwise. The Clear method wipes out the contents of the target collection. The IsReadOnly property indicates whether the instance is read-only or not. The ICollection<T> also provides a couple of properties with which to read information about a collection instance. The Count property will retrieve the number of items currently stored. And finally, Contains will search for the specified item in its contents and return either a Boolean true or false. Examine this sample code that illustrates operations on collections:

C#
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;

public sealed class Program {
public static void Main()
   {
            ICollection<int> myCollection = new Collection<int>();

            // First, add a few elements to the collection:
            myCollection.Add(105);
            myCollection.Add(232);
            myCollection.Add(350);

            // …And then delete one:
            myCollection.Remove(232);

            // Search for some specific elements:
            Console.WriteLine("Contains {0}? {1}", 105, myCollection.Contains(105));
            Console.WriteLine("Contains {0}? {1}", 232, myCollection.Contains(232));

            // Enumerate the collection’s contents:
            foreach (int i in myCollection)
                Console.WriteLine(i);

            // Lastly, copy the contents to an array so that we may iterate that:
            int[] myArray = new int[myCollection.Count];
            myCollection.CopyTo(myArray, 0);
            for (int i = 0; i < myArray.Length; i++)
                Console.WriteLine(myArray[i]);
        }
     }

Gives this output:

105
350
105
350

The result of executing this code is that myCollection.Contains(105) returns true, but myCollection.Contains(232) returns false (since we removed it).

Indexable Collections: (IList<T>)

The IList<T> interface drives from the ICollection<T> and adds a few members to support adding, removing, and accessing contents using a 0-based index.

C#
namespace System.Collections.Generic
{
  public interface IList<T>   :   ICollection<T>
    {
       // properties

          T this[int index]  {   get;    set;   }


       // methods
          int IndexOf(T item);
          void Insert(int index, T item);
          void RemoveAt(int index);

      }
}

This code illustrates the use of the members provided and exposed by the IList<T> interface:

C#
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
public sealed class Program {
 public static void Main()
        {
            IList<double> myList = new List<double>();

            // notice that we passing a data type to a class


            // First, we add, insert, and remove some items:
            myList.Add(10.54);
            myList.Add(209.2234);
            myList.Insert(1, 39.999);
            myList.Add(438.2);
            myList.Remove(10.54);

            // Then, we print some specific element indexes:
            Console.WriteLine("IndexOf {0} = {1}", 209.2234, myList.IndexOf(209.2234));
            Console.WriteLine("IndexOf {0} = {1}", 10.54, myList.IndexOf(10.54));

            // Lastly, we enumerate the list using Count and IList<t />’s indexer:
            for (int i = 0; i < myList.Count; i++)
                Console.WriteLine(myList[i]);
        }
     }

The output is as follows:

IndexOf 209.2234 = 1
IndexOf 10.54 = -1
39.999
209.2234
438.2

Dictionaries (IDictionary<TKey, TValue>)

A dictionary is a container with a collection of key and value associations. This data structure is also called an associative array, map, or hash table. With IDictionary<TKey<tkey, />,TValue> the type parameters TKey and TValue represent the type of keys and values it can store. So, for example, an IDictionary<string, int> is a dictionary that contains string keys that map to integer values:

C#
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;

       public sealed class Program {

       public static void Main()
        {
            IDictionary<string, decimal> salaryMap = new Dictionary<string, decimal>();

            // Add some entries into the dictionary:
            salaryMap.Add("Sean", 62250.5M);
            salaryMap.Add("Wolf", 16000.0M);
            salaryMap.Add("Jamie", 32900.99M);

            // Now, remove one:
            salaryMap.Remove("Wolf");

            // Check whether certain keys exist in the map:
            Console.WriteLine(salaryMap.ContainsKey("Sean")); // Prints ‘True’
            Console.WriteLine(salaryMap.ContainsKey("Steve")); // Prints ‘False’

            // Retrieve some values from the map:
            Console.WriteLine("{0:C}", salaryMap["Sean"]); // Prints ‘$62,250.50’
            Console.WriteLine("{0:C}", salaryMap["Jamie"]); // Prints ‘$32,900.99’

            // Now just iterate over the map and add up the values:
            decimal total = 0.0M;
            foreach (decimal d in salaryMap.Values)
                total += d;
            Console.WriteLine("{0:C}", total); // Prints ‘$95,151.49’

            // Iterating over map/value pairs:
            // (The wrong way)
            foreach (string key in salaryMap.Keys)
                Console.WriteLine("{0} == {1}", key, salaryMap[key]);

            // (The right way)
            foreach (KeyValuePair<string, decimal><string, /> kvp in salaryMap)
                Console.WriteLine("{0} == {1}", kvp.Key, kvp.Value);
        }
       }  

Outputs the following:

True
False
$62,250.50
$32,900.99
$95,151.49
Sean == 62250.5
Jamie == 32900.99
Sean == 62250.5
Jamie == 32900.99

Here is the basic prototype:

C#
namespace System.Collections.Generic
{
    public interface IDictionary<TKey, TValue> :
    ICollection<KeyValuePair<TKey,TValue>>

    {
        //   properties

        TValue this[TKey  key]   {  get;    set;  }
        ICollection<TKey>  Keys   {  get; }
        ICollection<TValue> Values   {  get:   }

        // methods

        void.Add(TKey key, TValue value);
        bool ContainsKey(TKey  key);
        bool Remove(TKey  key);
        bool TryGetValue(TKey key, TValue  value);
     }
     [Serializable, StructLayout(LayoutKind.Sequential)]
     public struct KeyValuePair<TKey, TValue>
     {
         public TKey Key;
         public TValue Value;
         public KeyValuePair(TKey key, TValue value);
         public override string ToString();
     }
} 

Each key-value association is represented using a KeyValuePair object. Now we recall the enumerators:

C#
using System;
using System.Collections;
namespace System.Collections.Generic
 {
  public interface IEnumerable<T> : IEnumerable
    {
        // methods

     IEnumerator<T> GetEnumerator();
     IEnumerator GetEnumerator(); // inherited from IEnumerable
     }

 public interface IEnumerator<T> : IDisposable, IEnumerator
    {
       // properties

        T Current { get; }
        object Current { get; }      // inherited from IEnumerator

       //  methods

     void Dispose();
     bool MoveNext();
     void Reset();
   }
}

Now examine the following code:

C#
 using System;
 using System.Collections.Generic;
 using System.Text;

  public class Program {
  public static void Main()
        {
            IEnumerable<string> enumerable = new string[] { "A", "B", "C" };

            // Short-hand form:
            foreach (string s in enumerable)
                Console.WriteLine(s);

            // Long-hand form:
            IEnumerator<string> enumerator = enumerable.GetEnumerator();
            while (enumerator.MoveNext())
            {
                string s = enumerator.Current;
                Console.WriteLine(s);
            }
        }
}

Outputs:

A
B
C
A
B
C

Recall that IEnumerator is, upon instantiation, dependent on another collection. Its sole purpose is to walk through the collection’s contents and read through it. The C# foreach construct relies on enumeration to access the contents of any enumerable collection. So when you write the following code:

C#
IEnumerable<string> enumerable  =  /* ……*/;
foreach (string s in enumerable)
  Console.WriteLine(s);

The C# compiler will actually emit a call to enumerable’s GetEnumerator method for you and will use the resulting enumeration to step through its contents.

C#
IEnumerable<string> enumerable  =  /* ……*/;

IEnumerator<string> enumerator = enumerable.GetEnumerator();

Custom Collection Base Type (Collection<T>)

System.Collections.ObjectModel.Collection<T> was designed to be the starting point of IList<T>. This assembly can be found in System.dll and not in mscorlib.dll. To create your own custom collection, you must subclass Collection<T><t> and do the following:

  • Decide which constructors you will support. Collection<T> comes with two: one default constructor and one that wraps an existing List<T>.
  • Customize the behavior of methods that modify the collection. The core of this functionality is the InsertItem method. Various public methods, such as Add and Insert, call through to this method. Similarly, SetItem is called when setting a collection’s contents via element index and RemoveItem is called when removing elements from the collection.
C#
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
 public class NotificationList<T> : Collection<T>
        {
            public event EventHandler<ItemInsertedArgs<T>> ItemAdded;

            protected override void InsertItem(int index, T item)
            {
                EventHandler<ItemInsertedArgs<T>> handler = ItemAdded;
                if (handler != null)
                {
                    handler(this, new ItemInsertedArgs<T>(index, item));
                }
                base.InsertItem(index, item);
            }
        }

        public class ItemInsertedArgs<T> : EventArgs
        {
            public int Index;
            public T Item;

            public ItemInsertedArgs(int index, T item)
            {
                this.Index = index;
                this.Item = item;
            }
        }
        public sealed class Program {
        public static void Main()
        {
            NotificationList<int> list = new NotificationList<int>();

            // Register an event handler:
            list.ItemAdded += delegate(object o, ItemInsertedArgs<int /> args) {
                Console.WriteLine("A new item was added to the list: {0} at index {1}",
                    args.Item, args.Index);
            };

            // Now insert some random numbers:
            Random r = new Random();
            for (int i = 0; i < 10; i++)
            {
                list.Add(r.Next());
            }
        }
}

Outputs this result:

A new item was added to the list: 1813778092 at index 0
A new item was added to the list: 166196501 at index 1
A new item was added to the list: 351546792 at index 2
A new item was added to the list: 1161666006 at index 3
A new item was added to the list: 822899709 at index 4
A new item was added to the list: 109809002 at index 5
A new item was added to the list: 1400891187 at index 6
A new item was added to the list: 2006077683 at index 7
A new item was added to the list: 1633381285 at index 8
A new item was added to the list: 1257751113 at index 9

Collections Implementations

The List<T> class in the most widely used implementation of IList<T>, providing an ordered, indexable collection of objects. It is dynamically sized and its capacity automatically grows to accommodate new items. When you construct a new list, you can, if you deem necessary, pass in its initial capacity as an argument using the List<T>(int capacity) constructor. Or you can create a new List<T> from any IEnumerable<T> object by using the List<T>(IEnumerable<T>) constructor. This allocates a new list into which it copies the contents of the target enumerable object. For example, you can create lists from other implementations of collections without having to manually copy their contents:

C#
// Creating a list from an array:
List<int> intList = new List<int>(new int[] { 3, 5, 15, 1003, 25 });

// Creating a list from a dictionary:
IDictionary<string, DateTime<string, /> dictionary = new Dictionary<string, DateTime<string, />();
List<KeyValuePair<string, DateTime>><keyvaluepair<string, /> keyValueList =
                new List<KeyValuePair<string, DateTime>><keyvaluepair<string, />(dictionary);

// Creating a list from a queue:
Queue<string> q = new Queue<string>(); //.
List<string> stringList = new List<string>(q);

and so forth

Examine this code:

C#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;

public sealed class App {
    private static int ConvertStringToInt(string input)
    {
        int result;
        if (!int.TryParse(input, out result))
            result = -1;
        return result;
    }

    public static void Main()
    {
        // Creating a list from an array:
        List<int> intList = new List<int>(new int[] { 3, 5, 15, 1003, 25 });

        // Creating a list from a dictionary:
        IDictionary<string, DateTime><string, /> dictionary = new Dictionary<string, DateTime><string, />();
        List<KeyValuePair<string, DateTime>><keyvaluepair<string, /> keyValueList =
        new List<KeyValuePair<string, DateTime>><keyvaluepair<string, />(dictionary);

        // Creating a list from a queue:
        Queue<string> q = new Queue<string>(); //.
        List<string> stringList = new List<string>(q);

        // `foreach' using an enumerator:
        foreach (int x in intList)
        Console.WriteLine(x);

        // Using the indexer:
        for (int i = 0; i < intList.Count; i++)
            Console.WriteLine(intList[i]);

        // Using the Action<T> delegate:
        intList.ForEach(delegate(int x) { Console.WriteLine(x); });

        // Converting a list:
        List<string> stringList1 = new List<string>(
        new string[] { "99", "182", "15" });
        List<int> intList1 = stringList1.ConvertAll<int>(Convert.ToInt32);

        List<string> stringList2 = new List<string>(
        new string[] { "99", "182", "invalid", "15" });
        List<int> intList2 = stringList2.ConvertAll<int>(ConvertStringToInt);
    }
}

Here is the output:

3
5
15
1003
25
3
5
15
1003
25
3
5
15
1003
25

While the above output does not seem to have any practical value, the use of generic classes (particularly in the area where arrays leave off – collections), can provide valuable means of creating, manipulating, iterating over, and performing other operations on collections. Consider these class files. They are compiled on the command line using the /target:library switch. Just type ‘type con > NameOfFile.cs and paste the code onto the console.

When pasted, press Control-Z:

C#
File: // Customer.cs
using System;
using System.Collections.Generic;
using System.Text;

public class Customer : System.IComparable {
        private int _id;
        private string _name;
        private string _rating;
        private static SortOrder _order;

        public enum SortOrder {
            Ascending = 0,
            Descending = 1
        }

        public Customer(int id, string name) : this(id, name, "Other") {
        }

        public Customer(int id, string name, string rating) {
            this._id = id;
            this._name = name;
            this._rating = rating;
        }

        public int Id {
            get { return this._id; }
            set { this._id = value; }
        }

        public string Name {
            get { return this._name; }
            set { this._name = value; }
        }

        public string Rating {
            get { return this._rating; }
            set { this._rating = value; }
        }

        public static SortOrder Order {
            get { return _order; }
            set { _order = value; }
        }

        public override bool Equals(Object obj) {
            bool retVal = false;
            if (obj != null) {
                Customer custObj = (Customer)obj;
                if ((custObj.Id == this.Id) &&
                    (custObj.Name.Equals(this.Name) &&
                    (custObj.Rating.Equals(this.Rating))))
                    retVal = true;
            }
            return retVal;
        }

        public override string ToString() {
            return this._id + ": " + this._name;
        }

        public int CompareTo(Object obj) {
            switch (_order) {
                case SortOrder.Ascending:
                    return this.Name.CompareTo(((Customer)obj).Name);
                case SortOrder.Descending:
                    return (((Customer)obj).Name).CompareTo(this.Name);
                default:
                    return this.Name.CompareTo(((Customer)obj).Name);
            }
        }
    }

File:// ComparerTest.cs

C#
using System;
using System.Collections.Generic;
using System.Text;

    public class ComparerTest {
        public void NameSortTest() {
            List<Customer> collCustList = new List<Customer>();
            collCustList.Add(new Customer(99, "Happy Gillmore", "Platinum"));
            collCustList.Add(new Customer(77, "Billy Madison", "Gold"));
            collCustList.Add(new Customer(55, "Bobby Boucher", "Gold"));
            collCustList.Add(new Customer(88, "Barry Egan", "Platinum"));
            collCustList.Add(new Customer(11, "Longfellow Deeds", "Other"));

            Console.Out.WriteLine("Before Sort:");
            foreach (Customer cust in collCustList)
                Console.Out.WriteLine(cust);

            Customer.Order = Customer.SortOrder.Ascending;
            collCustList.Sort(delegate(Customer cust1, Customer cust2) {
                return Comparer<Customer><customer />.Default.Compare(cust1, cust2);
            });

            Console.Out.WriteLine("After Ascending Sort:");
            foreach (Customer cust in collCustList)
                Console.Out.WriteLine(cust);

            Customer.Order = Customer.SortOrder.Descending;
            collCustList.Sort(delegate(Customer cust1, Customer cust2) {
                return Comparer<Customer><customer />.Default.Compare(cust1, cust2);
            });

            Console.Out.WriteLine("After Descending Sort:");
            foreach (Customer cust in collCustList)
                Console.Out.WriteLine(cust);
        }
    }

All of the files that comprise this project should be downloaded and extracted into a named project folder. Double-click the solution file, build the solution, and then click “run without debugging". The output should appear as something like this:

Val1
Val2
Val Changed
Val2
1: Sponge Bob
4: Inserted Person
2: Kim Possible
9: Fat Albert
1: Sponge Bob
3: George Jetson
4: Fred Flintsone
1: Sponge Bob
2: Kim Possible
3: George Jetson
4: Fred Flintsone
1: Sponge Bob
2: Kim Possible
4: Fred Flintsone
Added--> 1: Sponge Bob
Added--> 2: Kim Possible
Added--> 3: George Jetson
Added--> 4: Fred Flintsone
Added--> 5: Barney Rubble
Removed--> 2: Kim Possible
Removed--> 5: Barney Rubble

Deletion History
=================
5: Barney Rubble
2: Kim Possible
Current Count: 3
Old Capacity : 2
New Capacity : 4

Current Count: 5
Old Capacity : 4
New Capacity : 8

Current Count: 9
Old Capacity : 8
New Capacity : 16

Final Count     : 12
Final Capacity  : 16
After TrimToSize: 12
99: Happy Gillmore
77: Billy Madison
33: Longfellow Deeds
88: Sonny Koufax
44: Robbie Hart
99: Happy Gillmore
88: Barry Egan
Find Customer Id 22: Henry Roth
Find Customer Id 22 Index: 4
Gold Customer Found: 77: Billy Madison
Gold Customer Found: 55: Bobby Boucher
Find Last Platinum Customer: 88: Barry Egan
Range Customer: 44: Robbie Hart
Range Customer: 22: Henry Roth
Range Customer: 88: Barry Egan
Happy Gillmore ****
Billy Madison ***
Bobby Boucher ***
Barry Egan ****
Longfellow Deeds **
CHANGED NAME
Billy Madison ***
Bobby Boucher ***
Barry Egan ****
Longfellow Deeds **
Happy Gillmore ****
Billy Madison ***
Bobby Boucher ***
Barry Egan ****
Longfellow Deeds **
HAPPY GILLMORE
BILLY MADISON
BOBBY BOUCHER
BARRY EGAN
LONGFELLOW DEEDS
Before:
99: Happy Gillmore
77: Billy Madison
55: Bobby Boucher
88: Barry Egan
11: Longfellow Deeds
After:
11: Longfellow Deeds
55: Bobby Boucher
77: Billy Madison
88: Barry Egan
99: Happy Gillmore
Reversed:
99: Happy Gillmore
88: Barry Egan
77: Billy Madison
55: Bobby Boucher
11: Longfellow Deeds
Before Sort:
99: Happy Gillmore
77: Billy Madison
55: Bobby Boucher
88: Barry Egan
11: Longfellow Deeds
After Ascending Sort:
88: Barry Egan
77: Billy Madison
55: Bobby Boucher
99: Happy Gillmore
11: Longfellow Deeds
After Descending Sort:
11: Longfellow Deeds
99: Happy Gillmore
55: Bobby Boucher
77: Billy Madison
88: Barry Egan
99: Happy Gillmore
77: Billy Madison
55: Bobby Boucher
88: Barry Egan
99: Happy Gillmore
77: Billy Madison
55: Longfellow Deeds
88: Barry Egan
Dupe Customer Value: 55
Dupe Customer Id: 11
Dupe Customer Id: 55
Dupe Customer Id: 77
Dupe Customer Found: 11
Dupe Customer Found: 55
Dupe Customer Found: 77
55: Bobby Boucher
88: Barry Egan
11: Longfellow Deeds
Dequeued: 99: Happy Gillmore
Queue Item: 77: Billy Madison
Queue Item: 55: Bobby Boucher
Queue Item: 88: Barry Egan
Queue Peek: 77: Billy Madison
Queue Item: 77: Billy Madison
Queue Item: 55: Bobby Boucher
Queue Item: 88: Barry Egan
Queue Item: 88: Barry Egan
99: Happy Gillmore
77: NAME CHANGED
55: Bobby Boucher
88: Barry Egan
11: Longfellow Deeds
Customer Name: Happy Gillmore
Customer Name: Billy Madison
Customer Name: Bobby Boucher
Customer Name: Barry Egan
Customer->Bobby Boucher
Customer->Billy Madison
Customer->Barry Egan
Customer->Happy Gillmore
Customer->Bobby Boucher
Customer->Billy Madison
Customer->Happy Gillmore

This article was referenced from “Professional .NET 2.0 Generics", written by Tod Golding.

History

  • 9th June, 2009: Initial post

License

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


Written By
Software Developer Monroe Community
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralLINQ Pin
Asher Barak6-Jul-09 6:30
professionalAsher Barak6-Jul-09 6:30 
GeneralCustom Object's Equals method Pin
Stephen Inglish9-Jun-09 2:40
Stephen Inglish9-Jun-09 2:40 

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.