Solving 3 problems with the ShellTileSchedule

Are the problems with the Shell Tile Schedule? Yes there are, at least I’m in the impression that there are some problems. Although we have those problems I really like the ShellTileSchedule because it enables an app to have an updated tile without the requirement to write server-side code to notify the client for a new tile. The smallest schedule that’s supported is 1-hour. In that situation every hour the tile will be updated with a tile located on the web (static url, which can return a dynamic image of course).

There are three problems I identified so far.

1. You can’t get the status of the ShellTileSchedule. Worse, although you started the schedule it might be stopped because for whatever reason (ex. phone is on Airplane mode) the downloading of the tile failed.

2. You have to wait at least 1 hour before the tile is updated for the first time.

3. After you stopped the ShellTileSchedule, the tile will be the last downloaded tile forever. It would be better if automatically the original tile (from the .xap package) is put back.

Combined a diagram to show the problems.

image

Solution 1 for problem 1

Alright, we can’t get the status. So what do we do? We store it locally, in the Isolated Storage. I really like the approach that’s explained by Joost van Schaik who created some extension methods for storing the settings in the Isolated Storage. He’s storing and retrieving to the Phone State on the Tomb Stoning events: Activated and Deactivated. I did store and retrieve from Isolated on all events: Activated, Deactivated, Launching and Closing.

That’s all about making sure we have a status. But still this status won’t be updated when the schedule stopped. The Windows Phone team suggests to start the schedule on every application start. The code of the Application_Launching event in the App.xaml.cs could look like this.

private void Application_Launching(object sender, LaunchingEventArgs e)
{
    Settings = this.RetrieveFromIsolatedStorage<SettingsViewModel>() ?? new SettingsViewModel();
    if(Settings.TileUpdatesEnabled)
    {
        new ShellTileSchedule
        {
            Interval = UpdateInterval.EveryHour,
            MaxUpdateCount = 0,
            Recurrence = UpdateRecurrence.Interval,
            RemoteImageUri = new Uri(@"http://mark.mymonster.nl/Uploads/2010/12/servertile.png"),
            StartTime = DateTime.Now
        }.Start();
    }
}

Joost mentions that the ViewModelBase of MVVM Light isn’t serializable. So I created a basic ViewModelBase that has the functionality that I’m always using in ViewModelBase (RaisePropertyChanged) and decorated it with the DataContract attribute.

[DataContract]
public class ViewModelBase : INotifyPropertyChanged
{
    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    #endregion

    protected void RaisePropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

Solution 2 for problem 2

Not many people know it’s possible, but Matthijs Hoekstra from Microsoft helped me in the direction to solve this problem.

Solution: Send a Push Notification from the phone itself. And afterwards close the channel and start the ShellTileSchedule.

It’s important that you send the Push Notification before the actual call to the start the ShellTileSchedule because the ShellTileSchedule is a kind of NotificationChannel of which only one can exist.

A lot of articles have been written already about how to do Push Notifications. After opening a NotificationChannel you will get a url. After that you send some xml to that url. The xml that has to be send looks like this.

?<?xml version="1.0" encoding="utf-8"?>
<wp:Notification xmlns:wp="WPNotification">
  <wp:Tile>
    <wp:BackgroundImage>http://someserver/servertile.png</wp:BackgroundImage>
    <wp:Count>0</wp:Count>
  </wp:Tile>
</wp:Notification>

The setup of the xml in code:

public void SendTile(Uri notificationUrl, string tileUri, int? count, string title, Action onComplete)
{
    var stream = new MemoryStream();
    var settings = new XmlWriterSettings {Indent = true, Encoding = Encoding.UTF8};
    XmlWriter writer = XmlWriter.Create(stream, settings);
    writer.WriteStartDocument();
    writer.WriteStartElement("wp", "Notification", "WPNotification");
    writer.WriteStartElement("wp", "Tile", "WPNotification");
    if (!string.IsNullOrEmpty(tileUri))
    {
        writer.WriteStartElement("wp", "BackgroundImage", "WPNotification");
        writer.WriteValue(tileUri);
        writer.WriteEndElement();
    }
    if (count.HasValue)
    {
        writer.WriteStartElement("wp", "Count", "WPNotification");
        writer.WriteValue(count.ToString());
        writer.WriteEndElement();
    }
    if (!string.IsNullOrEmpty(title))
    {
        writer.WriteStartElement("wp", "Title", "WPNotification");
        writer.WriteValue(title);
        writer.WriteEndElement();
    }
    writer.WriteEndElement();
    writer.Close();
    byte[] payload = stream.ToArray();

	...
}

If you take a look at the Windows Phone 7 Training Kit you will see an example on how to do a Push Notification. It can be used almost one on one in the Windows Phone application itself. I removed some of the code (for the Raw and Toast Notifications) and refactored it to fit my needs.

So after the setup of the xml, the sending of the xml and setting of the HTTP headers looks like this:

public void SendTile(Uri notificationUrl, string tileUri, int? count, string title, Action onComplete)
{
	...
	
	byte[] payload = stream.ToArray();

    //Check the length of the payload and reject it if too long
    if (payload.Length > MaxPayloadLength)
        throw new ArgumentOutOfRangeException(
            string.Format("Payload is too long. Maximum payload size shouldn't exceed {0} bytes",
                            MaxPayloadLength));

    //Create and initialize the request object
    var request = (HttpWebRequest) WebRequest.Create(notificationUrl);
    request.Method = "POST";
    request.ContentType = "text/xml; charset=utf-8";
    //request.ContentLength = payload.Length;
    request.Headers["X-MessageID"] = Guid.NewGuid().ToString();
    request.Headers["X-NotificationClass"] = 1.ToString();
    request.Headers["X-WindowsPhone-Target"] = "token";

    request.BeginGetRequestStream(
        ar =>
            {
                //Once async call returns get the Stream object
                Stream requestStream = request.EndGetRequestStream(ar);

                //and start to write the payload to the stream asynchronously
                requestStream.BeginWrite(
                    payload, 0, payload.Length,
                    iar =>
                        {
                            //When the writing is done, close the stream
                            requestStream.EndWrite(iar);
                            requestStream.Close();

                            //and switch to receiving the response from MPNS
                            request.BeginGetResponse(
                                iarr =>
                                    {
                                        if (onComplete != null)
                                            onComplete();
                                    },
                                null);
                        },
                    null);
            },
        null);
}

Of course we shouldn’t forget the important part: Subscribing to the Notification channel. Take special notice to the Thread.Sleep in line 21. This is to make sure that the Tile update is completed before the unbinding, and more starting the ShellTileSchedule.

private void UpdateTileBeforeOperation(Uri imageUri, Action onComplete)
{
    HttpNotificationChannel channel = HttpNotificationChannel.Find("OneTime");
    if (channel != null)
        channel.Close();
    else
    {
        channel = new HttpNotificationChannel("OneTime");
        channel.ChannelUriUpdated +=
            (s, e) =>
                {
                    if (imageUri.IsAbsoluteUri)
                        channel.BindToShellTile(new Collection<Uri> {imageUri});
                    else
                        channel.BindToShellTile();

                    SendTile(e.ChannelUri, imageUri.ToString(), 0, "",
                                () =>
                                    {
                                        //Give it some time to let the update propagate
                                        Thread.Sleep(
                                            TimeSpan.FromSeconds(1));

                                        channel.UnbindToShellTile();
                                        channel.Close();
                                        //Do the operation
                                        if (onComplete != null)
                                            onComplete();
                                    }
                        );
                };
        channel.Open();
    }
}

Solution 3 for problem 3

The solution for problem 3 is similar to solution 2. But instead of a remote url the url is an relative url, local to the .xap file.

public void Stop(Action onComplete)
{
    UpdateTileBeforeOperation(new Uri("/Background.png", UriKind.Relative),
                                () => { if (onComplete != null) onComplete(); });
}

Again it allows you to include an action that will be called upon completion of the Stop method.

 

Full solution diagram

Alright, all problems solved. The diagram now looks like this.

image

Of course you want to have the full code for the SmartShellTileSchedule.

public class SmartShellTileSchedule
{
    private const int MaxPayloadLength = 1024;

    public UpdateRecurrence Recurrence { get; set; }

    public int MaxUpdateCount { get; set; }

    public DateTime StartTime { get; set; }

    public UpdateInterval Interval { get; set; }

    public Uri RemoteImageUri { get; set; }

    /// <summary>
    /// If the schedule is enabled (store this in application settings) this operation should be 
    /// called upon each application start.
    /// </summary>
    public void CheckForStart()
    {
        DelegateSchedule().Start();
    }

    /// <summary>
    /// This will enable the schedule and make sure the tile is updated immediately. Don't call 
    /// this operation on each application start.
    /// </summary>
    public void Start()
    {
        Start(null);
    }

    /// <summary>
    /// This will enable the schedule and make sure the tile is updated immediately. Don't call 
    /// this operation on each application start.
    /// </summary>
    /// <param name="onComplete">will be called upon completion</param>
    public void Start(Action onComplete)
    {
        UpdateTileBeforeOperation(RemoteImageUri, () =>
                                                        {
                                                            CheckForStart();
                                                            if (onComplete != null) onComplete();
                                                        });
    }

    /// <summary>
    /// This will stop the schedule and make sure the tile is replaced with the original logo-tile.
    /// Assumption is that the logo-tile is called "Background.png"
    /// </summary>
    public void Stop()
    {
        Stop(null);
    }

    /// <summary>
    /// This will stop the schedule and make sure the tile is replaced with the original logo-tile.
    /// Assumption is that the logo-tile is called "Background.png"
    /// </summary>
    /// <param name="onComplete">will be called upon completion</param>
    public void Stop(Action onComplete)
    {
        UpdateTileBeforeOperation(new Uri("/Background.png", UriKind.Relative),
                                    () => { if (onComplete != null) onComplete(); });
    }

    private void UpdateTileBeforeOperation(Uri imageUri, Action onComplete)
    {
        HttpNotificationChannel channel = HttpNotificationChannel.Find("OneTime");
        if (channel != null)
            channel.Close();
        else
        {
            channel = new HttpNotificationChannel("OneTime");
            channel.ChannelUriUpdated +=
                (s, e) =>
                    {
                        if (imageUri.IsAbsoluteUri)
                            channel.BindToShellTile(new Collection<Uri> {imageUri});
                        else
                            channel.BindToShellTile();

                        SendTile(e.ChannelUri, imageUri.ToString(), 0, "",
                                    () =>
                                        {
                                            //Give it some time to let the update propagate
                                            Thread.Sleep(
                                                TimeSpan.FromSeconds(1));

                                            channel.UnbindToShellTile();
                                            channel.Close();
                                            //Do the operation
                                            if (onComplete != null)
                                                onComplete();
                                        }
                            );
                    };
            channel.Open();
        }
    }

    private ShellTileSchedule DelegateSchedule()
    {
        return new ShellTileSchedule
                    {
                        Interval = Interval,
                        MaxUpdateCount = MaxUpdateCount,
                        Recurrence = Recurrence,
                        RemoteImageUri = RemoteImageUri,
                        StartTime = StartTime
                    };
    }

    public void SendTile(Uri notificationUrl, string tileUri, int? count, string title, Action onComplete)
    {
        var stream = new MemoryStream();
        var settings = new XmlWriterSettings {Indent = true, Encoding = Encoding.UTF8};
        XmlWriter writer = XmlWriter.Create(stream, settings);
        writer.WriteStartDocument();
        writer.WriteStartElement("wp", "Notification", "WPNotification");
        writer.WriteStartElement("wp", "Tile", "WPNotification");
        if (!string.IsNullOrEmpty(tileUri))
        {
            writer.WriteStartElement("wp", "BackgroundImage", "WPNotification");
            writer.WriteValue(tileUri);
            writer.WriteEndElement();
        }
        if (count.HasValue)
        {
            writer.WriteStartElement("wp", "Count", "WPNotification");
            writer.WriteValue(count.ToString());
            writer.WriteEndElement();
        }
        if (!string.IsNullOrEmpty(title))
        {
            writer.WriteStartElement("wp", "Title", "WPNotification");
            writer.WriteValue(title);
            writer.WriteEndElement();
        }
        writer.WriteEndElement();
        writer.Close();
        byte[] payload = stream.ToArray();

        //Check the length of the payload and reject it if too long
        if (payload.Length > MaxPayloadLength)
            throw new ArgumentOutOfRangeException(
                string.Format("Payload is too long. Maximum payload size shouldn't exceed {0} bytes",
                                MaxPayloadLength));

        //Create and initialize the request object
        var request = (HttpWebRequest) WebRequest.Create(notificationUrl);
        request.Method = "POST";
        request.ContentType = "text/xml; charset=utf-8";
        //request.ContentLength = payload.Length;
        request.Headers["X-MessageID"] = Guid.NewGuid().ToString();
        request.Headers["X-NotificationClass"] = 1.ToString();
        request.Headers["X-WindowsPhone-Target"] = "token";

        request.BeginGetRequestStream(
            ar =>
                {
                    //Once async call returns get the Stream object
                    Stream requestStream = request.EndGetRequestStream(ar);

                    //and start to write the payload to the stream asynchronously
                    requestStream.BeginWrite(
                        payload, 0, payload.Length,
                        iar =>
                            {
                                //When the writing is done, close the stream
                                requestStream.EndWrite(iar);
                                requestStream.Close();

                                //and switch to receiving the response from MPNS
                                request.BeginGetResponse(
                                    iarr =>
                                        {
                                            if (onComplete != null)
                                                onComplete();
                                        },
                                    null);
                            },
                        null);
                },
            null);
    }
}

Additional I also included my SettingsViewModel which is fully bindable.

[DataContract]
public class SettingsViewModel : ViewModelBase
{
    private ICommand _enforceTileUpdatesState;
    private bool _executing;
    private ICommand _setScheduleIfEnabled;
    private bool _tileUpdatesEnabled;

    [DataMember]
    public bool TileUpdatesEnabled
    {
        get { return _tileUpdatesEnabled; }
        set
        {
            if (value != _tileUpdatesEnabled)
            {
                _tileUpdatesEnabled = value;
                RaisePropertyChanged("TileUpdatesEnabled");
            }
        }
    }

    public bool Executing
    {
        get { return _executing; }
        set
        {
            if (value != _executing)
            {
                _executing = value;
                RaisePropertyChanged("Executing");
            }
        }
    }

    public ICommand SetScheduleIfEnabled
    {
        get
        {
            if (_setScheduleIfEnabled == null)
            {
                _setScheduleIfEnabled = new RelayCommand(
                    () =>
                        {
                            if (TileUpdatesEnabled)
                            {
                                Executing = true;
                                GetSchedule().CheckForStart();
                                Executing = false;
                            }
                        });
            }
            return _setScheduleIfEnabled;
        }
    }

    public ICommand EnforceTileUpdatesState
    {
        get
        {
            if (_enforceTileUpdatesState == null)
            {
                _enforceTileUpdatesState = new RelayCommand(
                    () =>
                        {
                            if (TileUpdatesEnabled)
                            {
                                Executing = true;
                                GetSchedule().Start(
                                    () =>
                                    Deployment.Current.Dispatcher.
                                        BeginInvoke(() => Executing = false));
                            }
                            else
                            {
                                Executing = true;
                                GetSchedule().Stop(
                                    () =>
                                    Deployment.Current.Dispatcher.
                                        BeginInvoke(() => Executing = false));
                            }
                        });
            }
            return _enforceTileUpdatesState;
        }
    }


    private SmartShellTileSchedule GetSchedule()
    {
        return new SmartShellTileSchedule
                    {
                        Interval = UpdateInterval.EveryHour,
                        RemoteImageUri =
                            new Uri(@"http://someserver/servertile.png"),
                        StartTime = DateTime.Now,
                        Recurrence = UpdateRecurrence.Interval
                    };
    }
}
  • Gravatar Axel December 19th, 2010 at 23:39
    Just curious: what makes you think 1 second is enough for the push server to process your request and send the notification back? In my experience (I've been running a live service using tile notifications for the last couple of weeks) the notification takes anywhere from 1 second to several HOURS to actually reach the phone. For some reason things tends to work much better in the emulator but your mileage may vary widely on an actual device. Be also prepared for ChannelUriUpdated never firing, or for the push server telling you that you are "temporarily disconnected" even if it's clearly not the case :-) Just my $0.002
  • Gravatar Arktronic January 4th, 2011 at 20:26
    FYI, this code seems to cause some odd issues in live Marketplace apps - http://forums.create.msdn.com/forums/t/71361.aspx
  • Gravatar Chris Hardy January 6th, 2011 at 21:56
    Hey Mark,

    For the first solution, having the tile schedule start like that means that if someone opens the app up every 50 minute or so, they will never get a live tile. Though this is not realistic, this coupled with the fact that when the phone is idle, it won't be checking for updates and if you're on WiFi only then it might not even have a connection when the phone comes out of idle then you're going to run into some very intermittent "live tiles".

    For the second solution, if a user wanted to turn off live tiles, you could always do a one time shell tile update to get the original live tile from the server, this seems to work pretty, it just won't be as instant as doing a push notification to get it.

    Hope this helps,

    ChrisNTR
  • Gravatar Joost van Schaik January 11th, 2011 at 15:16
    Hi Mark,

    Thanks for noticing my solution for tombstoning. Be aware that I more or less abandoned the approach using an XMLSerializer - I know use SilverligthSerializer by Mike Talbot which - binary - serializes about everything. Very cool. I'd recommend using it.
  • Gravatar Edo February 3rd, 2011 at 13:26
    Thanks Mark, really neat solution!

    I had only one problem (really hard to find out): when calling
    channel.BindToShellTile(new Collection { imageUri });
    it threw InvalidOperationException(Notification server temporary unavailable)
    and subsequent calls to
    HttpNotificationChannel channel = HttpNotificationChannel.Find("ChannelName");
    always threw InvalidOperationException(Notification server temporary unavailable)

    After a lot of debug, I found out that the problem was the length of Uri (including http://... ).
    If it's shorter than 130 chars all is ok, if longer it keeps on throwing exceptions.

    For what I've seen, it's enough to use the host in the Uri, so replacing
    channel.BindToShellTile(new Collection { imageUri });
    with
    channel.BindToShellTile(new Collection { (new UriBuilder(imageUri.Scheme, imageUri.Host, imageUri.Port)).Uri });
    solved the problem.

    Thanks again!
  • Gravatar Doug Rathbone March 4th, 2011 at 08:34
    I am having the same problem as Edo,

    except mine is actually failing all the way through as my Uri is too long for the tile. i don't have a way around this as i'm actually passing token values in the Uri and they are around 200 chars long - unless i store these for my users (bad security model) it won't work

    anyone else run into this?

    cheers
    Doug
  • Gravatar Manfred Dalmeijer June 13th, 2011 at 16:18
    This solution works very well in my Windows Phone 7 apps.
    There is one issue though: HttpNotificationChannel methods and the ShellTileSchedule methods can raise an InvalidOperationException (see http://msdn.microsoft.com/en-us/library/microsoft.phone.shell.shelltileschedule.start (VS.92).aspx).
    Since these exceptions occur very infrequently and my apps set the schedule every time they run, I chose a lazy solution: catch them with an empty (apart from logging) handler.
  • Gravatar abc June 27th, 2011 at 07:41
    can i get the code sample please
  • Gravatar YSL バッグ コピー April 6th, 2014 at 05:00
    上質なエナメルレザーを使用し、ステッチがアクセントとなったイヴサンローランのショルダーバッグです。光沢のあるブラックにゴールドの金具が高級感があります。定番のブラックは合わせやすく、シーンを選ばずお使い頂けます。ボックス付きなので保管の際も安心です。◇サイズ幅約23cm 縦約13cm マチ約3.5cmハンドル約40cm◇付属品:ボックス?コントロールカード?取扱説明書◇素材:レザー◇カラー:ブラック◇品番:296528 AB80O 737▼天然革を使用の為、レザー部分には小さな小傷や跡がある場合がございます。予めご了承下さい。▼モニターにより、色の見え方が実際の商品と異なることがございます。ご了承下さいませ。
  • [付属品] 箱(押し跡有り)、保存袋×2、替えリフト×2
  • Gravatar ルブタン メンズ 2013 April 14th, 2014 at 06:43
    明るいピンクがとってもキュート
  • Gravatar セリーヌ バッグ ラゲージ April 16th, 2014 at 04:08
    人気ブランドの旬なバッグを気軽に楽しめるプチプライスがLush (ラッシュ)の魅力!
  • Gravatar セリーヌ ファントム ラゲージ April 19th, 2014 at 05:32
    ◆ 色 :フラミンゴ
Gravatar