With just a little code and some prompting from people on Twitter, I was able to add support for FireFox, Safari, and IE7 to the paging demo that I wrote on http://xmldocs.net/sl2paging.  Basically I added a DispatcherTimer to my page class and every 200ms I check to see if the hash has changed.  It definitely isn't as elegant as the hashChanged event in IE8 but it works cross-browser. Here's how I did it.

  1. I added a DispatcherTimer to my page class.
  2. In the Page Loaded event handler, I start the timer if the browser User Agent does not have MSIE 8 in it.
  3. Each time the timer ticks I compare the last hash value with the current document's hash and if it's different, I navigate to the new page.

Please try it out in any supported browser (IE7, IE8, FireFox, Safari) and tell me if it works for you.  Should I change the polling interval to be greater than 200ms?

Here's the source code on MSDN Code Gallery.

Update

8/5/2008 - I wrote too soon. I got a report that this technique is not working on IE7.  I have been running the IE8 beta and I had assumed that since it worked in its IE7 emulation mode, it would work in IE7.  Back to the drawing board...

using System;
using System.Linq;
using System.Windows.Browser;
using System.Windows.Threading;
using System.Windows.Controls;
using System.Windows;

namespace SilverlightPaging
{
    /// <summary>
    /// Page Control
    /// </summary>
    public partial class Page : UserControl
    {
        string m_hash;
        DispatcherTimer m_timer = new DispatcherTimer();
        /// <summary>
        /// Default Constructor
        /// </summary>
        public Page()
        {
            InitializeComponent();
        }

        /// <summary>
        /// Page Loaded handler
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        /// <remarks>Ingnore the Code Analysis CA1811</remarks>
        private void PageLoaded(object sender, RoutedEventArgs e)
        {
            var info = HtmlPage.BrowserInformation;
            this.DataContext = info;
            
            m_hash = HtmlPage.Window.Eval("document.location.hash") as string;

            if (info.UserAgent.Contains("MSIE 8"))
            {
                HtmlPage.Window.AttachEvent("onhashchange", new EventHandler<HtmlEventArgs>(OnHashChange));
            }
            else
            {
                m_timer.Interval = new TimeSpan(0, 0,0,0, 200);

                m_timer.Tick += new EventHandler(m_timer_Tick);

                m_timer.Start();
            }

            NavigateToTab();
        }

        void m_timer_Tick(object sender, EventArgs e)
        {
            var hash = HtmlPage.Window.Eval("document.location.hash") as string;

            if (hash != m_hash)
            {
                m_hash = hash;

                NavigateToTab();
            }
        }

        private void NavigateToTab()
        {
            var hash = HtmlPage.Window.Eval("document.location.hash") as string;

            if (string.IsNullOrEmpty(hash))
            {
                return;
            }

            hash = hash.Substring(1);

            if (Tabs.SelectedItem != null)
            {
                var selectedItem = Tabs.SelectedItem as TabItem;

                if (string.Compare(selectedItem.Tag.ToString(), hash, StringComparison.OrdinalIgnoreCase) == 0)
                {
                    // Tab is already navigated to
                    return;
                }
            }

            // Get the tab items that have the hash in their Tag attribute
            var selectedItem1 = from item in Tabs.Items.Cast<TabItem>()
                                where string.Compare(item.Tag.ToString(), hash, StringComparison.OrdinalIgnoreCase) == 0
                               select item;

            Tabs.SelectedItem = selectedItem1.First();
        }

        void OnHashChange(object sender, HtmlEventArgs args)
        {
            NavigateToTab();
        }

        /// <summary>
        /// When the tab changes, set the document.location.hash and the document.Title
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        /// <remarks>Ingnore the Code Analysis CA1811</remarks>
        private void TabChanged(object sender, SelectionChangedEventArgs e)
        {
            if (Tabs == null)
            {
                //Tabs will be null during InitializeComponent()
                return;
            }

            TabItem item = Tabs.SelectedItem as TabItem;

            if (item == null)
            {
                return;
            }

            string evalText = string.Format(System.Globalization.CultureInfo.InvariantCulture,
                "document.location.hash=\"{0}\"", item.Tag);

            HtmlPage.Window.Eval(evalText);

            evalText = string.Format(System.Globalization.CultureInfo.InvariantCulture,
                "document.title=\"Silverlight Paging: {0} Tab\"", item.Tag);

            HtmlPage.Window.Eval(evalText);
        }
    }
}