Delay's Blog

Silverlight, WPF, Windows Phone, Web Platform, .NET, and more...

April, 2011

Posts
  • Delay's Blog

    "Those who cannot remember the past are condemned to repeat it." [WebBrowserExtensions.StringSource attached dependency property makes Silverlight/Windows Phone/WPF's WebBrowser control more XAML- and binding-friendly]

    • 10 Comments

    The WebBrowser control is available in Silverlight 4, Windows Phone 7, and all versions of WPF. It's mostly the same everywhere, though there are some specific differences to keep in mind when using it on Silverlight-based platforms. WebBrowser offers two ways to provide its content: by passing a URI or by passing a string with HTML text:

    1. If you have a URI, you can set the Source (dependency) property in code or XAML or you can call the Navigate(Uri) method from code.

      Aside: It's not clear to me what the Navigate(Uri) method enables that the Source property doesn't, but flexibility is nice, so I won't dwell on this. :)
    2. On the other hand, if you have a string, your only option is to call the NavigateToString(string) method from code.

      XAML and data-binding support for strings? Nope, not so much...

     

    I'm not sure why all three platforms have the same limitation, but I suspect there was a good reason at some point in time and maybe nobody has revisited the decision since then. Be that as it may, the brief research I did before writing this post suggests that a good number of people have been inconvenienced by the issue. Therefore, I've written a simple attached dependency property to add support for providing HTML strings in XAML via data binding!

    <phone:WebBrowser delay:WebBrowserExtensions.StringSource="{Binding MyProperty}"/>

    As you can see above, this functionality is made possible by the StringSource property which is exposed by the WebBrowserExtensions class. It's a fairly simple attached property that just passes its new value on to the WebBrowser's NavigateToString method to do the real work. For everyone's convenience, I've tried to make sure my StringSource implementation works on Silverlight 4, Windows Phone 7, and WPF.

    Aside: The StringSource property is read/write from code and XAML, but does not attempt to detect WebBrowser navigation by other means (and somehow "transform" the results into a corresponding HTML string). Therefore, if you're interleaving multiple navigation methods in the same application, reading from StringSource may not be correct - but writing to it should always work!

    Aside: Things are more complicated on Windows Phone because the WebBrowser implementation there throws exceptions if it gets touched outside the visual tree. Therefore, if WINDOWS_PHONE is defined (and by default it is for phone projects), this code catches the possible InvalidOperationException and deals with it by creating a handler for the WebBrowser's Loaded event that attempts to re-set the string once the control is known to be in the visual tree. If the second attempt fails, the exception is allowed to bubble out of the method. This seems to work nicely for the typical "string in XAML" scenario, though it's possible more complex scenarios will require a more involved workaround.

    My thanks go out to Roger Guess for trying an early version of the code and reminding me of this gotcha!

     

    To prove to ourselves that StringSource behaves as we intend, let's create the world's simplest RSS reader! All it will do is download a single RSS feed, parse it for the titles and content of each post, and display those titles in a ListBox. There'll be a WebBrowser control using StringSource to bind to the ListBox's SelectedItem property (all XAML; no code!), so that when a title is clicked, its content will automatically be displayed by the WebBrowser!

     

    Here's what it looks like on Silverlight (note that the sample must be run outside the browser because of network security access restrictions in Silverlight):

    WebBrowserExtensions on Silverlight

     

    And here's the same code running on Windows Phone:

    WebBrowserExtensions on Windows Phone

     

    And on WPF:

    WebBrowserExtensions on WPF

     

    [Click here to download the WebBrowserExtensions class and the samples above for Silverlight, Windows Phone, and WPF.]

     

    The StringSource attached dependency property is simple code for a simple purpose. It doesn't have a lot of bells and whistles, but it gets the job done nicely and fills a small gap in the platform. You won't always deal with HTML content directly, but when you do, StringSource makes it easy to combine the WebBrowser control with XAML and data binding!

  • Delay's Blog

    Don't shoot the messenger [A WebBrowserExtensions workaround for Windows Phone and a BestFitPanel tweak for infinite layout bounds on Windows Phone/Silverlight/WPF]

    • 5 Comments

    One of the neat things about sharing code with the community is hearing how people have learned from it or are using it in their own work. Of course, the more people use something, the more likely they are to identify problems with it - which is great because it provides an opportunity to improve things! This blog post is about addressing two issues that came up around some code I published recently.

     

    The Platform Workaround (WebBrowserExtensions)

    WebBrowserExtensions on Windows Phone

    Roger Guess contacted me a couple of days after I posted the WebBrowserExtensions code to report a problem he saw when using it on Windows Phone 7 with the platform's NavigationService in a scenario where the user could hit the Back button to return to a page with a WebBrowser control that had its content set by the WebBrowserExtensions.StringSource property. (Whew!) Instead of seeing the content that was there before, the control was blank! Sure enough, I was able to duplicate the problem after I knew the setup...

    My initial theory was that the WebBrowser was discarding its content during the navigation and not being reinitialized properly when it came back into view. Sure enough, some quick testing confirmed this was the case - and what's more, the same problem happens with the official Source property as well! That made me feel a little better because it suggests a bug with the platform's WebBrowser control rather than my own code. :)

    The workaround I came up with for StringSource (and that was kindly verified by Roger) should work just as well for the official Source property: I created an application-level event handler for the Loaded event on the WebBrowser and use that event to re-apply the correct content during the "back" navigation. I updated the Windows Phone sample application and added a new button/page to demonstrate the fix in action.

    If this scenario is possible with your application, please consider applying a similar workaround!

    Aside: Although it should be possible to apply the workaround to the WebBrowserExtensions code itself, I decided that wasn't ideal because of the event handler: the entire StringSource attached dependency property implementation is static, and tracking per-instance data from static code can be tricky. In this case, it would be necessary to ensure the Loaded event handler was added only once, that it was removed when necessary, and that it didn't introduce any memory leaks. Because such logic is often much easier at the application level and because the same basic workaround is necessary for the official WebBrowser.Source property and because it applies only to Windows Phone, it seemed best to leave the core WebBrowserExtensions implementation as-is.
    Further aside: This same scenario works fine on Silverlight 4, so it's another example of a Windows Phone quirk that needs to be worked around. (Recall from the previous post that it was already necessary to work around the fact that the Windows Phone WebBrowser implementation can't be touched outside the visual tree.) That's a shame because the scenario itself is reasonable and consistent with the platform recommendation to use NavigationService for everything. The fact that it seems broken for the "real" Source property as well makes me think other people will run into this, too. :(

    [Click here to download the WebBrowserExtensions class and samples for Silverlight, Windows Phone, and WPF.]

     

    The Layout Implementation Oversight (BestFitPanel)

    Eitan Gabay contacted me soon after I posted my BestFitPanel code to report an exception he saw when using one of the BestFitPanel classes as the ItemsPanel of a ListBox at design-time. I hadn't tried that particular configuration, but once I did, I saw the same message: "MeasureOverride of element 'Delay.MostBigPanel' should not return PositiveInfinity or NaN as its DesiredSize.". If you've dealt much with custom Panel implementations, this probably isn't all that surprising... Although coding layout is often straightforward, there can be a variety of edge cases depending on how the layout is done. (For example: only one child, no children, no available size, nested inside different kinds of parent containers, etc..)

    In this case, it turns out that the constraint passed to MeasureOverride included a value of double.PositiveInfinity and BestFitPanel was returning that same value. That isn't allowed because the MeasureOverride method of an element is supposed to return the smallest size the element can occupy without clipping - and nothing should require infinite size! (If you think about it, though, the scenario is a little wacky for BestFitPanel: what does it mean to make the best use of an infinite amount of space?)

    There are two parts to my fix for this problem. The first part is to skip calling the CalculateBestFit override for infinite bounds (it's unlikely to know what to do anyway) and to Measure all the children at the provided size instead. This ensures all children get a chance to measure during the measure pass - which some controls require in order to render correctly. The second part of the fix is to return a Size with the longest width and height of any child measured when infinite bounds are passed in. Because children are subject to the same rule about not returning an infinite value from Measure, this approach means BestFitPanel won't either and that the Panel will occupy an amount of space that's related to the size of its content (instead of being arbitrary like 0x0, 100x100, etc.).

    The combined effect of these changes is to fix the reported exception, provide a better design-time experience, and offer an more versatile run-time experience as well!

    All BestFitPanels overlapped

    [Click here to download the source code for all BestFitPanels along with sample projects for Silverlight, WPF, and Windows Phone.]

     

    The more a piece of code gets looked at and used, the more likely it is that potential problems are uncovered. It can be difficult to catch everything on your own, so it's fantastic to have a community of people looking at stuff and providing feedback when something doesn't work. Thanks again to Robert and Eitan for bringing these issues to my attention and for taking the time to try out early versions of each fix!

    I'm always hopeful people won't have problems with my code - but when they do, I really appreciate them taking the time to let me know! :)

  • Delay's Blog

    Images in a web page: meh... Images *in* a web page: cool! [Delay.Web.Helpers assembly now includes an ASP.NET web helper for data URIs (in addition to Amazon S3 blob/bucket support)]

    • 2 Comments
    Delay.Web.Helpers DataUri sample page

    The topic of "data URIs" came up on a discussion list I follow last week in the context of "I'm in the process of creating a page and have the bytes of an image from my database. Can I deliver them directly or must I go through a separate URL with WebImage?" And the response was that using a data URI would allow that page to deliver the image content inline. But while data URIs are fairly simple, there didn't seem to be a convenient way to use them from an ASP.NET MVC/Razor web page.

    Which was kind of fortuitous for me because I've been interested in learning more about data URIs for a while and it seemed that creating a web helper for this purpose would be fun. Better yet, I'd already released the Delay.Web.Helpers assembly (with support for Amazon S3 blob/bucket access), so I had the perfect place to put the new DataUri class once I wrote it! :)

    Aside: For those who aren't familiar, the WebImage class provides a variety of handy methods for dealing with images on the server - including a Write method for sending them to the user's browser. However, the Write method needs to be called from a dedicated page that serves up just the relevant image, so it isn't a solution for the original scenario.

     

    In case you've not heard of them before, data URIs are a kind of URL scheme documented by RFC 2397. They're quite simple, really - here's the relevant part of the specification:

    data:[<mediatype>][;base64],<data>
    
    dataurl    := "data:" [ mediatype ] [ ";base64" ] "," data
    mediatype  := [ type "/" subtype ] *( ";" parameter )
    data       := *urlchar
    parameter  := attribute "=" value
    

    It takes two pieces of information to create a data URI: the data and its media type (ex: "image/png"). (Although the media type appears optional above, it defaults to "text/plain" when absent - which is unsuitable for most common data URI scenarios.) Pretty much the only interesting thing you can do with data URIs on the server is write them, so the DataUri web helper exposes a single Write method with five flavors. The media type is always passed as a string (feel free to use the MediaTypeNames class to help here), but the data can be provided as a file name string, byte[], IEnumerable<byte>, or Stream. That's four methods; the fifth one takes just the file name string and infers the media type from the file's extension (ex: ".png" -> "image/png").

    Aside: Technically, it would be possible for the other methods to infer media type as well by examining the bytes of data. However, doing so would require quite a bit more work and would always be subject to error. On the other hand, inferring media type from the file's extension is computationally trivial and much more likely to be correct in practice.

     

    For an example of the DataUri helper in action, here's the Razor code to implement the "image from the database" scenario that started it all:

    @{
        IEnumerable<dynamic> databaseImages;
        using (var database = Database.Open("Delay.Web.Helpers.Sample.Database"))
        {
            databaseImages = database.Query("SELECT * FROM Images");
        }
    
        // ...
    
        foreach(var image in databaseImages)
        {
            <p>
                <img src="@DataUri.Write(image.Content, image.MediaType)" alt="@image.Name"
                     width="@image.Width" height="@image.Height" style="vertical-align:middle"/>
                @image.Name
            </p>
        }
    }

    Easy-peasy lemon-squeezy!

     

    And here's an example of using the file name-only override for media type inference:

    <script src="@DataUri.Write(Server.MapPath("Sample-Script.js"))" type="text/javascript"></script>

    Which comes out like this in the HTML that's sent to the browser:

    <script src="data:text/javascript;base64,77u/ZG9j...PicpOw==" type="text/javascript"></script>
    Aside: This particular example (using a data URI for a script file) doesn't render in all browsers. Specifically, Internet Explorer 8 (and earlier) blocks script delivered like this because of security concerns. Fortunately, Internet Explorer 9 has addressed those concerns and renders as expected. :)

     

    [Click here to download the Delay.Web.Helpers assembly, complete source code, automated tests, and the sample web site.]

    [Click here to go to the NuGet page for Delay.Web.Helpers which includes the DLL and its associated documentation/IntelliSense XML file.]

    [Click here to go to the NuGet page for the Delay.Web.Helpers.SampleWebSite which includes the sample site that demonstrates everything.]

     

    Data URIs are pretty neat things - though it's important to be aware they have their drawbacks as well. Fortunately, the Wikipedia article does a good job discussing the pros and cons, so I highly recommend looking it over before converting all your content. :) Creating a data URI manually isn't rocket science, but it is the kind of thing ASP.NET web helpers are perfectly suited for. If you're a base-64 nut, maybe you'll continue doing this by hand - but for everyone else, I hope the new DataUri class in the Delay.Web.Helpers assembly proves useful!

Page 1 of 1 (3 items)