Jay.Beavers: Enjoying life one line of C# at a time

  • Saving data from an XBap Sidebar Gadget

    Our next step is to save our notes so they’re still around the next time you restart Sidebar or the computer.  We’ll need to figure out:

    ·         How to get the data out of the RichTextBox

    ·         How to save and load the data to disk.  Saving data to disk is a little tricky because our Gadget is running in the partially trusted Internet Zone because it’s an XBap.  You can find out more about what partial trust means on MSDN or Karen Corby’s blog.

    ·         When to save and load the data for “optimal user experience”.  This will become more important later on when we start saving notes to a web service since more than one computer may be updating the notes on the web service and we want to minimize problems where data is overwritten or lost.

    How to get data out of the RichTextBox.

    Silly me, I thought this was going to be easy.  I’d be able to write some code like “object notes = richTextBox.Content” or “richTextBox.Content = notes”.  RichTextBox supports more advanced save/load scenario than this which means the code is a bit more complex.

    You can Save/Load a portion of the RichTextBox contents, so a simple .Save/.Load method isn’t sufficient.  Instead, we have to specify the portion of the content we want to work on.

    TextRange range = new TextRange(
        richTextBox.Document.ContentStart,
        richTextBox.Document.ContentEnd
        );

    You can also Save/Load from a variety of formats.  We’ll choose the “most native”, Xaml.

    range.Save(stream, DataFormats.Xaml)

    Add the necessary glue, and we have:

    private void Save()

    {

        RichTextBox richTextBox = this.FindName("NotesBox") as RichTextBox;

        TextRange range = new TextRange(
            richTextBox.Document.ContentStart,
            richTextBox.Document.ContentEnd
            );

     

        try

        {

            using (stream)

            {

                range.Save(stream, DataFormats.Xaml);

            }

     

            trace.TraceInformation("Notes Saved");

            richTextBox.Background = Brushes.Green; // Signal save

     

        }

        catch (Exception ex)

        {

            trace.TraceInformation(ex.ToString());

            richTextBox.Background = Brushes.Red; // Signal error

        }

    }

    Load ends up being very similar:

    private void Load()

    {

        RichTextBox richTextBox = this.FindName("NotesBox") as RichTextBox;

        TextRange range = new TextRange(
            richTextBox.Document.ContentStart,
            richTextBox.Document.ContentEnd
            );

     

        try

        {

            using (stream)

                if (stream.Length > 0)

                    range.Load(stream, DataFormats.Xaml);

     

            trace.TraceInformation("Notes loaded");

            richTextBox.Background = Brushes.AntiqueWhite; // Signal load

        }

        catch (Exception ex)

        {

            trace.TraceInformation(ex.ToString());

            richTextBox.Background = Brushes.Red; // Signal error

        }

    }

    How to Load and Save from Disk in Partial Trust

    Alright – the above code looks good, but there’s that undefined ‘stream’ variable hanging around.  How can we create a FileStream to use here that won’t throw a security exception in partial trust?  The answer is an API created just for this purpose called IsolatedStorage (see MSDN or an article by Chris Tavares ).  Briefly, Isolated Storage is an API that:

    ·         Provides an API that can open a FileStream without triggering an IOPermission exception under partial trust

    ·         Reads & Writes to a folder hidden deep in the user profile directory (on Vista, C:\Users\Username\AppData\Local\IsolatedStorage).  It *cannot* be used to read or write any file not stored under here.

    ·         Provides further Isolation to prevent different Applications and such from being able to see or overwrite each other’s data.  There’s plenty of detail here we’re going to ignore for the purposes of this article because it’s out of scope (that’s a subtle joke, folks).

    I know what you’re thinking now.  Hmm, that seems complicated.  Do I really want to have to understand this complex new API just to be able to save some data from a Gadget?  Let me save you some anxiety.  While IsolatedStorage sounds complicated in design (because it is), using it is quite simple.  We’re going to use the code:

    IsolatedStorageFileStream stream = new IsolatedStorageFileStream(

        "Notes", // Name for the stream

        FileMode.OpenOrCreate, // Open the file if it exists, create it if it doesn't

        FileAccess.ReadWrite, // Allow me to read & write to the file

        IsolatedStorageFile.GetUserStoreForApplication() // Isolate by user & application

        );

     

    It turns out that using IsolatedStorage is actually a bit simpler than using a “normal” FileStream or FileInfo object.  With this API, you don’t need to worry about finding a special directory (how do I get My Documents again?), other people mucking with your data, whether Vista UAC is going to cause your application to fail because you tried to write a file next to the .Exe and the process is no longer running with Administrator permissions, or whether your app has Full Trust or Partial Trust permissions.  That’s all been taken care of for you by the API.

    When to Save & Load

    I found it a little confusing to figure out when I should save and load content.  Loading upon startup is obvious, but we don’t want to Save on shutdown or we may lose data.

    UI applications are notorious for not guaranteeing that your shutdown code like _Unloaded will be called and the .NET Garbage Collector ‘lazy deallocation’ design means that sometimes your object is simply torn down without notification as the process is exiting.  The 'normal' way to approach this in .NET is to use IDisposable, but that won't work here because we don't control the code that's used to load our control and the default WPF behavior doesn't Dispose() content on shutdown.  This is made even less deterministic when we have an XBap running in an IE instance running in a Sidebar.  What we really need to do is ‘save on finished changing’.  When I’m in a situation like this, the way I approach the problem is to play around and see what works.  What events are called when?  Which event corresponds best to how I *think* the application should behave?  You can see now why I change the background color in the Save and Load events – I wanted a very explicit visual cue of when things were happening as I played around.  Eventually I settled on saving in _LostKeyboardFocus which fires whenever you click on some other window after editing.

    Scaling the Loading

    I’ll address a future need now since we’re on the topic of Save & Load.  When we start to support “Save/Load from web service” in the future, the contents of my notes may change because I made a change on another machine.  One way to detect this would be to hold a network connection (e.g. a socket) open and use some form of notification mechanism (e.g. a callback in WCF terms) to tell me when something has been changed on the server.  This is an excellent design but it doesn’t scale very well.  Since my intention is to host a “notes web service” on my home computer over my waaay too slow DSL line and then publish it to this blog, I really didn’t want to design a “detect change” mechanism that would cause my server (which, by definition, is one of my old desktops for a cheapskate like me) to hold network connections open to potentially thousands of blog readers.  I also didn’t want to use a ‘polling’ design that would cause thousands of computers, which may be idle, to poll my server on a regular basis.  I decided to go with a “load when the user is interested” approach.

    I defined “user is interested” as “user mouses over the notes form”.  With some more experimentation, I settled on an algorithm of when Page_MouseEnter fires and !IsKeyboardFocusWithin.  This wasn’t very intuitive and the eventing interaction of an XBap within a Gadget within a Sidebar was a bit strange so I followed the "how I *think* the application should behave" approach mentioned above to figure this out.  Once I decided on this algorithm, it pretty much makes sense when put into words.  “If you mouse over the form and you’re not already in the process of editing the notes, reload.”  When I left off the !IsKeyboardFocusWithin, I kept hitting the ‘less than useful’ behavior that I would be in the middle of editing the notes with the keyboard, I’d jiggle the mouse off and back onto the form, and this would fire Page_MouseExit/Page_MouseEnter which would load the last saved copy of my notes losing all the changes I was making.

    What does the code look like now?

    using System;

    using System.Diagnostics;

    using System.IO;

    using System.IO.IsolatedStorage;

    using System.Windows;

    using System.Windows.Controls;

    using System.Windows.Documents;

    using System.Windows.Input;

    using System.Windows.Media;

     

    namespace NetNotes

    {

        public partial class Page1 : Page

        {

            TraceSource trace = new TraceSource("NetNotes", SourceLevels.Information);

     

            public Page1()

            {

                InitializeComponent();

     

                trace.TraceInformation("Initialized");

     

                Load();

     

                MouseEnter += Page1_MouseEnter;

                LostKeyboardFocus += Page1_LostKeyboardFocus;

            }

     

            void Page1_MouseEnter(object sender, MouseEventArgs e)

            {

                if (!IsKeyboardFocusWithin)

                    Load();

            }

     

            void Page1_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)

            {

                Save();

            }

     

            private void Save()

            {

                RichTextBox richTextBox = this.FindName("NotesBox") as RichTextBox;

                TextRange range = new TextRange(
                    richTextBox.Document.ContentStart,
                    richTextBox.Document.ContentEnd
                    );

     

                try

                {

                    using (

                        IsolatedStorageFileStream stream = new IsolatedStorageFileStream(

                            "Notes",
                            FileMode.OpenOrCreate,                 
                            FileAccess.ReadWrite,                                        
                            IsolatedStorageFile.GetUserStoreForApplication()                       
                            ))

                        range.Save(stream, DataFormats.Xaml);

     

                    trace.TraceInformation("Notes Saved");

                    richTextBox.Background = Brushes.Green; // Signal save

     

                }

                catch (Exception ex)

                {

                    trace.TraceInformation(ex.ToString());

                    richTextBox.Background = Brushes.Red; // Signal error

                }

            }

     

            private void Load()

            {

                RichTextBox richTextBox = this.FindName("NotesBox") as RichTextBox;

                TextRange range = new TextRange(
                    richTextBox.Document.ContentStart,
                    richTextBox.Document.ContentEnd
                    );

     

                try

                {

                    using (
                        IsolatedStorageFileStream stream = new IsolatedStorageFileStream(
                            "Notes",
                            FileMode.OpenOrCreate,
                            FileAccess.Read,
                            IsolatedStorageFile.GetUserStoreForApplication()
                        ))

                        if (stream.Length > 0)

                            range.Load(stream, DataFormats.Xaml);

     

                    trace.TraceInformation("Notes loaded");

                    richTextBox.Background = Brushes.AntiqueWhite; // Signal load to user

                }

                catch (Exception ex)

                {

                    trace.TraceInformation(ex.ToString());

                    richTextBox.Background = Brushes.Red; // Signal error

                }

            }

        }

    }

  • Debugging the Xbap Sidebar Gadget

    Attaching the Visual Studio Debugger to your Gadget running in the Sidebar

    When working with WPF code in XBap form, things work a little differently than you may expect.  Your code doesn't run in the Sidebar.exe process, it runs in a separate process named PresentationHost.exe.  This isn't something new to the Vista Sidebar, this is how Internet Explorer launches XBaps in general.  For more detail, see this post on the topic.  Essentially what this means for Gadget debugging is that you open the Gadget in the Sidebar, open the csproj and set your breakpoints, attach to PresentationHost.exe, and then exercise the code.

    Occasionally this method won't work for you because you need to debug a problem that happens during the startup or initialization portion of your code.  In this case, there's a trick you can use to get attached to PresentationHost.exe before your XBap is opened.  The steps are:

    • Kill any existing PresentationHost.exe process.  Note that this will bring down any running XBaps in a bad way.  I use the command line: taskkill /f /im presentationhost.exe /fi "STATUS eq RUNNING"
    • Start a new PresentationHost.exe process in a special "wait for XBap" mode: start PresentationHost.exe -embedding
    • Open your project and use Debug -> Attach to Process -> PresentationHost.exe.  Make sure to select "Attach To: Managed code, Native code" because before the XBap is loaded, there is no managed code in the PresentationHost.exe process.
    • Set your breakpoints in your code or turn on Debug -> Exceptions per your debugging needs.
    • Open the Gadget in Sidebar

    I've occasionally run into problems where the debugger thinks it's attached but my breakpoints don't fire while running under Vista.  This post talks about a bad interaction between Vista UAC and PresentationHost.exe that may be causing this and how to fix it.  The fix (killing IE first) has always worked for me.

    printf Debugging .NET

    Hmm, the above sounds complex, no?  It can certainly get tiring after a while, especially when coding in a tight loop.  Another common technique in debugging is to use what in the olden days we referred to as "printf debugging".  Before the days of "attach debugger to process" and breakpoints, you sometimes had to debug by writing status messages out as the program ran.  Since this was commonly used in the early days of C programming and since the statement used to write to the console in C was named printf, this was given the nickname "printf debugging".

    We could use this technique, writing a string to the console with Console.WriteLine, if we were a console application but we're not.  Our next best approach is to use one of the two System.Diagnostic APIs (System.Diagnostics.Debug.WriteLine or System.Diagnostics.TraceSource.TraceInformation) to write status strings and then to view these strings using a debug output viewer like Sysinternals DebugView.  This is simple enough to do and I tend to use System.Diagnostics.TraceSource in my code because this is a very flexible API which is useful for trace logging in general, not just printf debugging.  A simple example of adding a trace statement to Page1.xaml.cs looks like:

    using System;

    using System.Diagnostics;

    using System.Windows.Controls;

     

    namespace NetNotes

    {

        /// <summary>

        /// Interaction logic for Page1.xaml

        /// </summary>

     

        public partial class Page1 : Page

        {

            TraceSource trace = new TraceSource("NetNotes", SourceLevels.Information);

     

            public Page1()

            {

                InitializeComponent();

     

                trace.TraceInformation("Initialized");

            }

        }

    }

  • Creating the simple XBap Sidebar Gadget

    Creating a new XBap project  

    First we start off with a generic XBap project, as so: 

    Adding a RichTextBox 

    Modify the contents of Page1.xaml to contain a RichTextBox rather than a <Grid> as so:

    <Page x:Class="NetNotes.Page1"

        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        Title="Page1"

        >

      <RichTextBox

          Name="NotesBox"

          HorizontalAlignment="Stretch" VerticalAlignment="Stretch" DockPanel.Dock="Top"

          Background="Beige"

          VerticalScrollBarVisibility="Auto"

          FontFamily="Arial" FontSize="12"

        />

    </Page>

    We have a functional, if extremely primitive, XBap application that can take notes.  Hit F5 to try it out, you'll see an Internet Explorer window open up with a blank beige background.  You can type in the window and take notes.

    Clearing the ClickOnce Cache

    One of the fun things about developing XBap or ClickOnce applications that you'll run into is cache versioning conflicts.  Behind the scenes an XBap is simply a fancy ClickOnce application.  One of the nice features ClickOnce brings is automatic installation of the application to an application cache, similar in concept to how Internet Explorer will auto-install an ActiveX control to its cache.  When you're developing ClickOnce or XBap applications and generating a new version with each build, you can fill up this cache rather quickly.  I've also seen times when the versioning gets out of hand and you start to see launch errors as the cache gets confused.  To help with both these issues, I clear the cache before building the application using the "mage.exe" tool which is part of the ClickOnce tool set.

    Clearing the ClickOnce Cache

     The full command line I add to my pre-build step (on my Vista x64 development computer) is:

    "C:\Program Files (x86)\Microsoft Visual Studio 8\SDK\v2.0\Bin\mage.exe" -cc

    You'll need to adjust this to suit your environment.  For example, on an x86 computer using default installation locations, the command would be:

    "C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\mage.exe" -cc

    Adding the Sidebar Gadget Glue

    Next we need to take out XBap and wrap it in the right files to make Vista Sidebar recognize it as a gadget.  This involves two steps -- first we need to create an HTML page that will host the XBap (because gadgets know how to show .HTML but not .XBAP) and second we need to create the gadget.xml manifest file.  First create a file called main.html in the VS project with the contents:

    <html xmlns="http://www.w3.org/1999/xhtml">

        <head>

            <style type="text/css">

                body {width:130; height:200; background:black; margin:0;}

            </style>

        </head>

        <body>

            <iframe width="128" height="200" src="NetNotes.xbap" />

        </body>

    </html>

    The amount of space the XBap will have to display is controlled by this file.  The width is pretty well set by the Sidebar itself, so you cannot get yourself extra space there but the height is up to you.  Note that I used a slightly smaller amount of width on my iframe than on my body.  This was on purpose -- it exposes the gadget controls.  If I set the widths to the exact same value, the close button, settings button, etc. did not pop out from the gadget for me.

    The second file we need is gadget.xml.  A very simple form of this looks like:

    <?xml version="1.0" encoding="utf-8" ?>

    <gadget>

      <name>Net Notes</name>

      <namespace>Beavers.Jay.NetNotes</namespace>

      <version>0.1.0.0</version>

      <hosts>

        <host name="sidebar">

          <base type="HTML" apiVersion="1.0.0" src="Main.html" />

          <permissions>Full</permissions>

          <platform minPlatformVersion="1.0" />

        </host>

      </hosts>

    </gadget>

    Both of these files should be added to the project with Build Action: Content and Copy to Output Directory: Copy always.

    Installing the Gadget

    The final part is getting the Gadget running in the sidebar.  This takes two steps: installing the files and adding the gadget.  Normally you'd install the files in the way the Sidebar SDK tells you: package everything from the bin/release directory into a zip file, rename it .gadget, and launch the .gadget file from the Explorer or IE.  This adds an additional step that slows down the development/debugging process if you're trying to rapidly turn around code, so I install the gadget via a post build step in the project, like so:

    Install the Gadget

    The full command line is:

    xcopy /y "$(TargetDir)*.*" "$(UserProfile)\AppData\Local\Microsoft\Windows Sidebar\Gadgets\$(ProjectName).gadget\"

    Adding the Gadget

    Finally, we're to:

    Add Gadget Hello Gadget

    OK, that's all for today.  In summary, we've:

    • Created an XBap and modified it to contain a RichTextBox that can take notes
    • Added "Gadget Glue" to the XBap to expose it to the Vista Sidebar
    • Set up build steps to auto-clean and auto-install the Gadget
  • Creating the NetNotes Gadget for Windows Vista

    Why? 

    I like the Notes gadget that comes with Windows Vista's new Sidebar.  I use it all the time to replace my old, ah, "organization skill" of keeping a copy of Notepad up and running in the corner of my screen with my todo list and thoughts of the day.  But every once in a while I get annoyed with it.  My basic pet peeves are:

    • It's too small.  If I have more than 4-5 short lines of content, it doesn't fit.  I don't like the "add a page" mechanism -- what I want is a little more real estate.
    • It doesn't roam.  I use a few computers over the course of the day.  I'd like my todo list to move around with me.

    Well, I write code for a living.  If I see something I don't like I should fix it, no?  So I did.  And I thought I'd share with you my adventures along the way.

    How?

    I'm a developer in the Education Products group here at Microsoft.  For my day job, I play with technologies like .NET 3.0 (WPF and WCF in particular).  I figured I would go ahead and use these to create my gadget.  So my approach will be:

    • Create a note taking UI using WPF, specifically an XBAP (XAML Browser Application) Page containing a RichTextBox.
    • Put together the glue that will take my XBAP and expose it as a Sidebar Gadget.
    • Add simple Save/Load events that store the notes to Isolated Storage on the local computer.
    • Create a simple WCF Service that I can use to store my Notes on my home computer.
    • Authenticate to the service using CardSpace to make sure my Todo list stays private.

    I'll work through each of these steps in a series of posts that show the details of putting these technologies together.


© 2009 Microsoft Corporation. All rights reserved. Terms of Use  |  Trademarks  |  Privacy Statement
Microsoft
Page view tracker