Tuesday, August 10, 2010

Silverlight's RichTextBox.Xaml vs. CurrentCulture

There appears to be a bug in the current version (4) of Silverlight and its RichTextBox control.

The RichTextBox.Xaml property contains a XAML representation of the box's content in form of a System.String. This property is both settable and gettable, which means that there must be serialization and deserialization happening on some level.
The problem is, that serialization (getting the RichTextBox.Xaml property) handles cultural differences differently than deserialization (setting the RichTextBox.Xaml property).

In my case RichTextBox.Xaml.get produces XAML with comma as the decimal separator (correct for my culture), but attempting to set the same XAML back to RichTextBox.Xaml throws an exception - it expects dots (US culture), not commas.

I've solved the problem by setting
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;

Labels: , , ,

Saturday, August 1, 2009

"Binding" to ListBoxItem's IsSelected property from its template in Silverlight

It is not really a binding, but it does the trick:

<ListBox Name="MyListBox" ItemContainerStyle="{StaticResource MyListBoxItemStyle}" >

<Style x:Key="MyListBoxItemStyle" TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border Name="Border" Padding="2" Loaded="Template_Loaded">
<ContentPresenter />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

private void Template_Loaded(object sender, RoutedEventArgs e)
{
Border border = (Border)sender;
ListBoxItem listBoxItem = (ListBoxItem)VisualTreeHelper.GetParent(border);

MyListBox.SelectionChanged +=
// this will be executed for every item whenever selection changes
delegate(object s1, SelectionChangedEventArgs e1)
{
if (listBoxItem.IsSelected)
{
// do stuff to "border" or "listBoxItem"
}
else
{
...
}
};
}

Labels: , , ,

Tuesday, July 28, 2009

Focus a Silverlight control on startup

I found two solutions to this problem on the web and neither of them worked for me. I finally combined both of them to make it work:

In the aspx page, include

<script type="text/javascript">
window.onload = function()
{
setTimeout(function() { document.getElementById('Xaml1').focus(); }, 1);
}
</script>

And then in Page.cs class as a response to Loaded event:

Dispatcher.BeginInvoke(delegate{myControl.Focus();});

Labels: ,

Tuesday, February 3, 2009

Silverlight: generic.xaml & OnApplyTemplate issues

In Silverlight, unlike WPF, the overriden OnApplyTemplate() method usually (but not always) gets called AFTER the Loaded event handler. This can be a source of many issues since you usually expect your control to fully load before handling the Loaded event. To prevent this behavior, you can call ApplyTemplate() within the body of the Loaded event handler, which will load the control template synchronously. What if it returns 'false' and your OnApplyTemplate doesn't get called at all?

You have created your custom control's class, put the style in Themes/generic.xaml resource dictionary, but you still keep getting the same XamlParserError exception on startup. Some investigation shows that your overriden OnApplyTemplate method never gets called. You try to invoke it manually by calling ApplyTemplate() in the Loaded event hander, but still no luck.

The solution is to fix your generic.xaml file - there is something wrong with it and neither Visual Studio nor Silverlight runtime will tell you what it is. Just comment the whole file out (except the style for your custom control, but comment out all its content) and you will see that now the OnApplyTemplate gets called.

Now begin uncommenting parts of the generic.xaml until you find what is causing the issue.

Labels:

Friday, December 26, 2008

Generating and downloading an XPS document using ASP.NET

Windows Presentation Foundation includes a new API for generating XPS documents. This article describes the steps involved in generating an XPS document on the server side of an ASP.NET web application using WPF and sending the resulting document to the client browser.

What we need to do is create a flow document, convert it to XPS and send it to the client.

Step 1 - Creating a FlowDocument on the server

Creating a FlowDocument object from scratch is as simple as calling the constructor:

FlowDocument flowDocument = new FlowDocument();
This gives us an empty FlowDocument that we can fill with content. The content is represented by a collection of Blocks. Filling the Blocks collection manually will probably suffice for very simple documents
Paragraph p1 = new Paragraph();
p1.Inlines.Add("This is the text");
p1.Inlines.Add("of the first paragraph");
Section section1 = new Section(p1);
flowDocument.Blocks.Add(section1);

but in a real project, we will most likely need some kind of a FlowDocument generator and/or have a predefined document template that we will fill with data. The natural format for such template is a XAML file.
<FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Section>
    <Section FontFamily="Times New Roman">
      <Paragraph Fontcolor: rgb(139, 0, 0);">18" FontWeight="Bold" >@placeholder1</Paragraph>
      <Paragraph Fontcolor: rgb(139, 0, 0);">10">
        @placeholder2
        Lorem ipsum dolor sit amet, consectetur adipisicing elit,
        sed do eiusmod tempor incididunt ut labore et dolore
        magna aliqua.
      </Paragraph>
    </Section>
</FlowDocument>
@placeholder1 and @placeholder2 will later be replaced by live data.

The template file can be saved (for example) as a global resource and later accessed by calling

string pageTemplate = (string)HttpContext.GetGlobalResourceObject("Resources", "MyTemplate");
Now that we have the template in memory as a string, it is a good time to replace the placeholders with real data and convert the final template to a real FlowDocument object
pageTemplate = pageTemplate.Replace("@placeholder1", MyData1);
pageTemplate = pageTemplate.Replace("@placeholder2", MyData2);
FlowDocument flowDocument = (FlowDocument)XamlReader.Parse(pageTemplate);
The placeholder replacement process is really lame here, but you get the point...

At this point, we have a filled-in FlowDocument object in memory ready to be converted to XPS.


Step 2 - Converting a FlowDocument to an (in-memory) XPS file

We start with a FlowDocument object and end up with an XPS file saved as an array of bytes. A code snippet is worth a thousand words here:
public static byte[] FlowDocumentToXPS(FlowDocument flowDocument, int width, int height)
{
    MemoryStream stream = new MemoryStream();

    // create a package
    using (Package package = Package.Open(stream, FileMode.Create))
    {
        // create an empty XPS document            
        using (XpsDocument xpsDoc = new XpsDocument(package, CompressionOption.Maximum))
        {
            // create a serialization manager
            XpsSerializationManager rsm = new XpsSerializationManager(new XpsPackagingPolicy(xpsDoc), false);
            // retrieve document paginator
            DocumentPaginator paginator = ((IDocumentPaginatorSource)flowDocument).DocumentPaginator;

            // set page size
            paginator.PageSize = new System.Windows.Size(width, height);       
            // save as XPS
            rsm.SaveAsXaml(paginator);
            rsm.Commit();
        }
        return stream.ToArray();
    }
}

Step 3 - Sending an in-memory file to the client browser

If we want to generate the XPS file (or any other kind of file) based on a client request, it is very inconvenient to have to save the file on the server and give the client its URI.

What we will do instead is create a hidden form on the web page and send the XPS file to the client as a response to submitting this form.

The hidden form can be very simple, all we need to include is the parameters that specify the XPS (or other) file to retrieve.

<form id="generateFileForm" action="DownloadFile.aspx" method="post">
    <input runat="server" type="hidden" id="id" />
</form>

Once we fill in the "id" parameter and submit the form

HtmlDocument doc = HtmlPage.Document;
HtmlElement id = doc.GetElementById("id");
id.SetAttribute("value", MyDocumentId);
doc.Submit("generateFileForm");

the code-behind of DownloadFile.aspx then does the trick

public partial class DownloadFile : System.Web.UI.Page
{
    [StaSyncOperationBehavior]
    protected void Page_Load(object sender, EventArgs e)
    {
        string sid = Request.Form["id"];     
        byte[] bytes = GetXpsFileBytes(sid);
        Response.Clear();
        Response.ContentType = "application/octet-stream";
        Response.AddHeader("Content-Disposition", "attachment; filename=document.xps");
        Response.OutputStream.Write(bytes, 0, bytes.Length);
        Response.Flush();
        Response.Close();
    }
}

The [StaSyncOperationBehavior] attribute was developed by Scott Seely and you can read about it here. UPDATE: The blog no longer seems to work, but here is the source code for the attribute.
To ensure that the response generation runs on a STA thread, we also need to set the AspCompat attribute on the DownloadFile.aspx page like this

<%@ Page AspCompat="true" Language="C#" AutoEventWireup="true" CodeBehind="DownloadFile.aspx.cs" Inherits="MyNamespace.DownloadFile" %>

Conclusion

The good thing about this solution is that nothing is saved to disk on the server side and that the client page does not reload when requesting the document, which means that you can happily use it to generate and download documents in Silverlight applications.

I hope this experience of mine will be useful to someone and if you have any questions or comments, please let me know in the discussion.

j.

Labels: , , ,