A Rich HTML TextBlock for WPF

This article -proudly- presents a WPF control that displays rich text from an HTML source: it has a Text dependency property that you can bind to a HTML-formatted string. It's designed to display -not edit- rich information messages like warnings, error messages, and other feedback e.g. from a localizable resource file. Here are a couple of examples, on the left side is the original HTML, on the right side is how it's displayed by the control:

Formatting:

Font sizes:

Colors:

Some issues with WPF's native RichTextBox control.

The HtmlTextBlock control is based on WPF's native RichTextBox control. This was not an obvious choice, since RichTextBox was actually designed as an editor control: by typing in the box and pressing toolbar buttons, a FlowDocument is created and displayed. This FlowDocument is accessible through the Document property, which unfortunately is

  • very complex to manipulate, compared to a formatted HTML or XAML string (you can't store it in a resource file), and
  • not a dependency property.

On the other hand, the FlowDocument has powerful text formatting features, so it really makes sense to try to leverage these.

Making a bindable RichTextBox

If you grab your favorite search engine and type "WPF Bindable RichTextBox" you'll find plenty of implementations. This one on CodePlex nicely explains the problems ànd solutions. Basically all you need to do is write a dependency property that is wrapped around the RichtTextBox' FlowDocument whilst converting it to and from a XAML-formatted string. Here's how this process looks like in code:

Dependency Property

These code snippets define the dependency property:

public static readonly DependencyProperty TextProperty = DependencyProperty.Register(

    "Text",

    typeof(string),

    typeof(RichTextBox),

    new FrameworkPropertyMetadata(

        String.Empty,

        FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,

        new PropertyChangedCallback(OnTextPropertyChanged),

        new CoerceValueCallback(CoerceTextProperty),

        true,

        System.Windows.Data.UpdateSourceTrigger.LostFocus));

 

public string Text

{

    get { return (string)GetValue(TextProperty); }

    set { SetValue(TextProperty, value); }

}

Conversion from XAML to FlowDocument

This method takes a XAML-formatted string and loads it into a FlowDocument:

public void SetText(System.Windows.Documents.FlowDocument document, string text)

{

    try

    {

        TextRange tr = new TextRange(document.ContentStart, document.ContentEnd);

        using (MemoryStream ms = new MemoryStream(Encoding.ASCII.GetBytes(text)))

        {

            tr.Load(ms, DataFormats.Xaml);

        }

    }

    catch

    {

        throw new InvalidDataException("data provided is not in the correct Xaml format.");

    }

}

Conversion from FlowDocument to XAML

This function does the opposite. It saves a FlowDocument as a XAML-formatted string:

public string GetText(System.Windows.Documents.FlowDocument document)

{

    TextRange tr = new TextRange(document.ContentStart, document.ContentEnd);

    using (MemoryStream ms = new MemoryStream())

    {

        tr.Save(ms, DataFormats.Xaml);

        return ASCIIEncoding.Default.GetString(ms.ToArray());

    }

}

The above code snippets originate from this RichtTextBox implementation from CodePlex (more about that later).

From TextBox to TextBlock

Since my control is a display panel instead of an edit box, I had to tweak the code in several places. Some extra properties were set in the constructor:

public RichTextBox()

{

    Loaded += RichTextBox_Loaded;

 

    //Added

    this.IsReadOnly = true;

    this.Focusable = false;

}

The original CodePlex control was not 100% percent bindable to a source: it was just Initializable from a source. After the source provided the initial value, all other updates were ignored. That was not compatible with my requirements: I wanted to bind the control to a dynamic value, like changing status information or an on-the-fly-localizable message. So I removed the code that inhibited long-term binding: the definition and all references and manipulation of the _textHasLoaded variable. Now the control does more, with less code.

All you need to do is bind the textbox to its source:

<uc:RichTextBox

   Text="{Binding Message}"

</uc:RichTextBox>

And on every update of the source, the displayed text will also be updated:

this.Message = "The report was sent to <b>your local printer</b>.<br/><br/>Press <u style='color:red'>Enter</u> to continue or <u style='color:red'>Back</u> to print again.";

 

Here's the result:

Converting HTML to XAML, and back

The native WPF RichTextBox nicely displays plain text, RTF, and XAML, but no HTML. Fortunately a lot of attempts were already made to convert HTML into XAML and back. The code I'm currently using comes from MSDN. Its probably not the world's best converter, but it simply does what I was looking for: formatting (bold, italic, underline, size, color, and line breaks). It's not the best solution if you want to properly display tables, lists, headers and so. A more powerful convertor can be found on CodePlex in the HTML 2 RTB XAML project, which in its turn is based on HTML Agility Pack. Unfortunately the conversion code in the former project is tightly coupled to (Silverlight) controls. I didn't have the time to refactor this. (Take that as a hint Smile)

Making the RichTextBox understand and speak HTML

The RichTextBox from the Extended WPF Toolkit -again from CodePlex- not only has a Text dependency property for data binding. It also introduces the concept of Text Formatters, allowing to declaratively and programmatically specify the format of the content. The three canonical Text Formatters are included; PlainTextFormatter, RtfFormatter, and XamlFormatter.

All I did was taking the code of the XamlFormatter and plugged in the Html converter. I also replaced the ASCII encoding by UTF8 encoding, since French text wasn't displayed correctly.

Here's the full code of the HTML-to-FlowDocument-and-back converter:

public class HtmlFormatter : ITextFormatter

{

    public string GetText(System.Windows.Documents.FlowDocument document)

    {

        TextRange tr = new TextRange(document.ContentStart, document.ContentEnd);

        using (MemoryStream ms = new MemoryStream())

        {

            tr.Save(ms, DataFormats.Xaml);

            return HtmlFromXamlConverter.ConvertXamlToHtml(UTF8Encoding.Default.GetString(ms.ToArray()));

        }

    }

 

    public void SetText(System.Windows.Documents.FlowDocument document, string text)

    {

        text = HtmlToXamlConverter.ConvertHtmlToXaml(text, false);

        try

        {

            TextRange tr = new TextRange(document.ContentStart, document.ContentEnd);

            using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(text)))

            {

                tr.Load(ms, DataFormats.Xaml);

            }

        }

        catch

        {

            throw new InvalidDataException("data provided is not in the correct Html format.");

        }

    }

}

Architecture

The following UML class diagram reveals all major component of the architecture:

Source Code

The included project contains the control, and a sample client.

Here's how the client application looks like:

Here's the code: U2UConsult.HtmlTextBlock.Sample.zip (91,62 kb)

Enjoy!


Getting and setting the Transaction Isolation Level on a SQL Entity Connection

This article explains how to get, set, and reset the transaction isolation level on a SQL and Entity connection. In a previous article I already explained how important it is to explicitly set the appropriate isolation level when using transactions. I'm sure you're not going to wrap each and every database call in an explicit transaction (TransactionScope is simply too heavy to wrap around a simple SELECT statement). Nevertheless you should realize that every single T-SQL statement that you launch from your application will run in a transaction, hence will behave according to a transaction isolation level. 

If you don't explicitely start a transaction or use a transaction scope, then SQL Server will run the statement as a transaction on its own. You don't want your SQL commands to read unofficial data (in the READ UNCOMMITTED level) or apply too heavy locks (in the SERIALIZABLE level) on the database, so you want to make sure that you're running with the correct isolation level. This article explains how to do this.

Setting the Transaction Isolation Level

Setting the appropriate isolation level on a session/connection is done with a standard T-SQL statement, like the following:

T-SQL

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ


It's easy to write an extension method on the SqlConnection and EntityConnection classes that allows you to do this call in C#, like this:

C#

using (AdventureWorks2008Entities model = new AdventureWorks2008Entities())

{

    model.Connection.Open();

 

    // Explicitely set isolation level

    model.Connection.SetIsolationLevel(IsolationLevel.ReadUncommitted);

 

    // Your stuff here ...

}


Here's the whole extension method. I implemented it against IDbConnection to cover all connection types. The attached Visual Studio solution contains the full code:

C#

public static void SetIsolationLevel(this IDbConnection connection, IsolationLevel isolationLevel)

{

    if (isolationLevel == IsolationLevel.Unspecified || isolationLevel == IsolationLevel.Chaos)

    {

        throw new Exception(string.Format("Isolation Level '{0}' can not be set.", isolationLevel.ToString()));

    }

 

    if (connection is EntityConnection)

    {

        SqlConnection sqlConnection = (connection as EntityConnection).StoreConnection as SqlConnection;

        sqlConnection.SetIsolationLevel(isolationLevel);

    }

    else if (connection is SqlConnection)

    {

        IDbCommand command = connection.CreateCommand();

        command.CommandText = string.Format("SET TRANSACTION ISOLATION LEVEL {0}", isolationLevels[isolationLevel]);

        command.ExecuteNonQuery();

    }

}

 

Getting the Transaction Isolation Level

If you want to retrieve the current isolation level on your connection, you have to first figure out how to do this in T-SQL. Unfortunately there is no standard @@ISOLATIONLEVEL function or so. Here's how to do it:

T-SQL

SELECT CASE transaction_isolation_level

          WHEN 0 THEN 'Unspecified'

          WHEN 1 THEN 'Read Uncommitted'

          WHEN 2 THEN 'Read Committed'

          WHEN 3 THEN 'Repeatable Read'

          WHEN 4 THEN 'Serializable'

          WHEN 5 THEN 'Snapshot'

       END AS [Transaction Isolation Level]

  FROM sys.dm_exec_sessions

 WHERE session_id = @@SPID


Altough you're querying a dynamic management view, the code requires no extra SQL Permissions (not even VIEW SERVER STATE). A user can always query his own sessions.

Again, you can easily wrap this into an extension method, that can be called like this:

C#

using (AdventureWorks2008Entities model = new AdventureWorks2008Entities())

{

    model.Connection.Open();

 

    // Get isolation level

    // Probably returns 'ReadUncommitted' (due to connection pooling)

    MessageBox.Show(model.Connection.GetIsolationLevel().ToString());

}


If you run the same code from the attached project, you'll indeed notice that the returned isolation level will be -most probably- READ UNCOMMITTED. This is because we -most probably- reuse the connection from the SetIsolationLevel() sample. As I already mentioned in a previous article, the transaction isolation level is NOT reset on pooled connections. So even if you're not explicitly use transactions, you still should still set the appropriate transaction isolation level. There's no default you can rely on.

OK, here's the corresponding extension method:

C#

public static IsolationLevel GetIsolationLevel(this IDbConnection connection)

{

    string query =

        @"SELECT CASE transaction_isolation_level

                    WHEN 0 THEN 'Unspecified'

                    WHEN 1 THEN 'ReadUncommitted'

                    WHEN 2 THEN 'ReadCommitted'

                    WHEN 3 THEN 'RepeatableRead'

                    WHEN 4 THEN 'Serializable'

                    WHEN 5 THEN 'Snapshot'

                    END AS [Transaction Isolation Level]

            FROM sys.dm_exec_sessions

            WHERE session_id = @@SPID";

 

    if (connection is EntityConnection)

    {

        return (connection as EntityConnection).StoreConnection.GetIsolationLevel();

    }

    else if (connection is SqlConnection)

    {

        IDbCommand command = connection.CreateCommand();

        command.CommandText = query;

        string result = command.ExecuteScalar().ToString();

 

        return (IsolationLevel)Enum.Parse(typeof(IsolationLevel), result);

    }

 

    return IsolationLevel.Unspecified;

}


Simple and powerful, isn't it? Stuff like this should ship with the framework!

Temporarily using a Transaction Isolation Level

With the new GetIsolationLevel() and SetIsolationLevel() methods it becomes easy to set the isolation level to execute some commands, and then reset the level to its original value. I wrapped these calls in a class implementing IDisposable so you can apply the using statement, like this:

C#

using (AdventureWorks2008Entities model = new AdventureWorks2008Entities())

{

    model.Connection.Open();

 

    // Set and reset isolation level

    using (TransactionIsolationLevel inner = new TransactionIsolationLevel(model.Connection, IsolationLevel.Snapshot))

    {

        // Your stuff here ...

    }

}


Again, the code is very straightforward. All you need is a constructor, a Dispose-method, and a variable to store the original isolation level:

C#

/// <summary>

/// Transaction Isolation Level.

/// </summary>

public class TransactionIsolationLevel : IDisposable

{

    /// <summary>

    /// The database connection.

    /// </summary>

    private IDbConnection connection;

 

    /// <summary>

    /// Original isolation level of the connection.

    /// </summary>

    private IsolationLevel originalIsolationLevel;

 

    /// <summary>

    /// Initializes a new instance of the TransactionIsolationLevel class.

    /// </summary>

    /// <param name="connection">Database connection.</param>

    /// <param name="isolationLevel">Required isolation level.</param>

    public TransactionIsolationLevel(IDbConnection connection, IsolationLevel isolationLevel)

    {

        this.connection = connection;

        this.originalIsolationLevel = this.connection.GetIsolationLevel();

        this.connection.SetIsolationLevel(isolationLevel);

    }

 

    /// <summary>

    /// Resets the isolation level back to the original value.

    /// </summary>

    public void Dispose()

    {

        this.connection.SetIsolationLevel(this.originalIsolationLevel);

    }

}

 

Source Code

The attached project contains the extension methods, the IDisposable class, and some demo calls against a local AdventureWorks database:

Here it is: U2UConsult.SQL.TransactionIsolationLevel.Sample.zip (87,53 kb)

Enjoy!


Download the U2U brochure

Download Brochure

Receive the U2U Newsletter. Submit your email address:
 
 


 


Search

rss  RSS

Tags

Recent posts

None

Archive

Blogroll