Showing posts with label AttachedProperty. Show all posts
Showing posts with label AttachedProperty. Show all posts

Saturday, 23 November 2013

RichText in a TextBlock

This project contains examples for Silverlight for providing formatting within a text block, either as a behaviour or attached property.  I would have liked to create custom control that derived from TextBlock but the class is sealed in Silverlight.

Source code:  https://github.com/stevenh77/RichTextBlockExample

image

<UserControl x:Class="RichTextBlockExample.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:local="clr-namespace:RichTextBlockExample"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">

<Grid x:Name="LayoutRoot" Background="White">
<StackPanel>
<TextBlock x:Name="TextBlockUsingBehaviour">
<i:Interaction.Behaviors>
<local:RichTextBlockBehaviour/>
</i:Interaction.Behaviors>
</TextBlock>

<TextBlock x:Name="TextBlockUsingAttachedProperty" local:SupportRichText.RichText="" />
</StackPanel>
</Grid>
</UserControl>

namespace RichTextBlockExample
{
public partial class MainPage
{
public MainPage()
{
InitializeComponent();

string output = "Testing <bold>formatted</bold> text <underline>with</underline> a <italic>textblock</italic>";
TextBlockUsingBehaviour.Text = output;
TextBlockUsingAttachedProperty.SetValue(SupportRichText.RichTextProperty, output);
}
}
}

Behaviour


using System.Windows;
using System.Windows.Controls;
using System.Collections.Generic;
using System.Windows.Documents;
using System.Windows.Interactivity;

namespace RichTextBlockExample
{
public class RichTextBlockBehaviour : Behavior<TextBlock>
{
private PropertyListener propertyListener;
protected override void OnAttached()
{
base.OnAttached();
propertyListener = new PropertyListener();
propertyListener.ListenForChange("Text", this.AssociatedObject, TextBlock_TextChanged);
}

private void TextBlock_TextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var textBlock = sender as TextBlock;
if (textBlock != null)
{
textBlock.Inlines.Clear();
textBlock.Inlines.Add(Traverse(e.NewValue.ToString()));
}
}

protected override void OnDetaching()
{
base.OnDetaching();
}

private static Inline Traverse(string value)
{
// Get the sections/inlines
string[] sections = SplitIntoSections(value);

// Check for grouping
if (sections.Length.Equals(1))
{
string section = sections[0];
string token; // E.g <Bold>
int tokenStart, tokenEnd; // Where the token/section starts and ends.

// Check for token
if (GetTokenInfo(section, out token, out tokenStart, out tokenEnd))
{
// Get the content to further examination
string content = token.Length.Equals(tokenEnd - tokenStart)
? null
: section.Substring(token.Length, section.Length - 1 - token.Length * 2);

switch (token.ToLower())
{
case "<bold>":
return new Run() { Text = content, FontWeight = FontWeights.Bold };

case "<italic>":
return new Run() { Text = content, FontStyle = FontStyles.Italic };

case "<underline>":
return new Run() { Text = content, TextDecorations = TextDecorations.Underline };

case "<linebreak/>":
return new LineBreak();

default:
return new Run() { Text = content };
}
}
else return new Run() { Text = section };
}
else // Group together
{
Span span = new Span();

foreach (string section in sections) span.Inlines.Add(Traverse(section));

return span;
}
}

/// <summary>
/// Examines the passed string and find the first token, where it begins and where it ends.
/// </summary>
/// <param name="value">The string to examine.</param>
/// <param name="token">The found token.</param>
/// <param name="startIndex">Where the token begins.</param>
/// <param name="endIndex">Where the end-token ends.</param>
/// <returns>True if a token was found.</returns>
private static bool GetTokenInfo(string value, out string token, out int startIndex, out int endIndex)
{
token = null;
endIndex = -1;

startIndex = value.IndexOf("<");
int startTokenEndIndex = value.IndexOf(">");

// No token here
if (startIndex < 0) return false;

// No token here
if (startTokenEndIndex < 0) return false;

token = value.Substring(startIndex, startTokenEndIndex - startIndex + 1);

// Check for closed token. E.g. <LineBreak/>
if (token.EndsWith("/>"))
{
endIndex = startIndex + token.Length;
return true;
}

string endToken = token.Insert(1, "/");

// Detect nesting;
int nesting = 0;
int temp_startTokenIndex = -1;
int temp_endTokenIndex = -1;
int pos = 0;
do
{
temp_startTokenIndex = value.IndexOf(token, pos);
temp_endTokenIndex = value.IndexOf(endToken, pos);

if (temp_startTokenIndex >= 0 && temp_startTokenIndex < temp_endTokenIndex)
{
nesting++;
pos = temp_startTokenIndex + token.Length;
}
else if (temp_endTokenIndex >= 0 && nesting > 0)
{
nesting--;
pos = temp_endTokenIndex + endToken.Length;
}
else // Invalid tokenized string
return false;

}
while (nesting > 0);

endIndex = pos;

return true;
}

/// <summary>
/// Splits the string into sections of tokens and regular text.
/// </summary>
/// <param name="value">The string to split.</param>
/// <returns>An array with the sections.</returns>
private static string[] SplitIntoSections(string value)
{
List<string> sections = new List<string>();

while (!string.IsNullOrEmpty(value))
{
string token;
int tokenStartIndex, tokenEndIndex;

// Check if this is a token section
if (GetTokenInfo(value, out token, out tokenStartIndex, out tokenEndIndex))
{
// Add pretext if the token isn't from the start
if (tokenStartIndex > 0) sections.Add(value.Substring(0, tokenStartIndex));

sections.Add(value.Substring(tokenStartIndex, tokenEndIndex - tokenStartIndex));
value = value.Substring(tokenEndIndex); // Trim away
}
else
{
// No tokens, just add the text
sections.Add(value);
value = null;
}
}

return sections.ToArray();
}
}
}

Attached Property


using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;

namespace RichTextBlockExample
{
public class SupportRichText
{
public static string GetRichText(DependencyObject obj)
{
return (string)obj.GetValue(RichTextProperty);
}

public static void SetRichText(DependencyObject obj, string value)
{
obj.SetValue(RichTextProperty, value);
}

public static readonly DependencyProperty RichTextProperty =
DependencyProperty.RegisterAttached(
"RichText",
typeof(string),
typeof(SupportRichText),
new PropertyMetadata("", RichTextChanged));

private static Inline Traverse(string value)
{
// Get the sections/inlines
string[] sections = SplitIntoSections(value);

// Check for grouping
if (sections.Length.Equals(1))
{
string section = sections[0];
string token; // E.g <Bold>
int tokenStart, tokenEnd; // Where the token/section starts and ends.

// Check for token
if (GetTokenInfo(section, out token, out tokenStart, out tokenEnd))
{
// Get the content to further examination
string content = token.Length.Equals(tokenEnd - tokenStart)
? null
: section.Substring(token.Length, section.Length - 1 - token.Length * 2);

switch (token.ToLower())
{
case "<bold>":
return new Run() { Text = content, FontWeight = FontWeights.Bold };

case "<italic>":
return new Run() { Text = content, FontStyle = FontStyles.Italic };

case "<underline>":
return new Run() { Text = content, TextDecorations = TextDecorations.Underline };

case "<linebreak/>":
return new LineBreak();

default:
return new Run() { Text = content };
}
}
else return new Run() { Text = section };
}
else // Group together
{
Span span = new Span();

foreach (string section in sections) span.Inlines.Add(Traverse(section));

return span;
}
}

/// <summary>
/// Examines the passed string and find the first token, where it begins and where it ends.
/// </summary>
/// <param name="value">The string to examine.</param>
/// <param name="token">The found token.</param>
/// <param name="startIndex">Where the token begins.</param>
/// <param name="endIndex">Where the end-token ends.</param>
/// <returns>True if a token was found.</returns>
private static bool GetTokenInfo(string value, out string token, out int startIndex, out int endIndex)
{
token = null;
endIndex = -1;

startIndex = value.IndexOf("<");
int startTokenEndIndex = value.IndexOf(">");

// No token here
if (startIndex < 0) return false;

// No token here
if (startTokenEndIndex < 0) return false;

token = value.Substring(startIndex, startTokenEndIndex - startIndex + 1);

// Check for closed token. E.g. <LineBreak/>
if (token.EndsWith("/>"))
{
endIndex = startIndex + token.Length;
return true;
}

string endToken = token.Insert(1, "/");

// Detect nesting;
int nesting = 0;
int temp_startTokenIndex = -1;
int temp_endTokenIndex = -1;
int pos = 0;
do
{
temp_startTokenIndex = value.IndexOf(token, pos);
temp_endTokenIndex = value.IndexOf(endToken, pos);

if (temp_startTokenIndex >= 0 && temp_startTokenIndex < temp_endTokenIndex)
{
nesting++;
pos = temp_startTokenIndex + token.Length;
}
else if (temp_endTokenIndex >= 0 && nesting > 0)
{
nesting--;
pos = temp_endTokenIndex + endToken.Length;
}
else // Invalid tokenized string
return false;

}
while (nesting > 0);

endIndex = pos;

return true;
}

/// <summary>
/// Splits the string into sections of tokens and regular text.
/// </summary>
/// <param name="value">The string to split.</param>
/// <returns>An array with the sections.</returns>
private static string[] SplitIntoSections(string value)
{
var sections = new List<string>();
while (!string.IsNullOrEmpty(value))
{
string token;
int tokenStartIndex, tokenEndIndex;

// Check if this is a token section
if (GetTokenInfo(value, out token, out tokenStartIndex, out tokenEndIndex))
{
// Add pretext if the token isn't from the start
if (tokenStartIndex > 0) sections.Add(value.Substring(0, tokenStartIndex));

sections.Add(value.Substring(tokenStartIndex, tokenEndIndex - tokenStartIndex));
value = value.Substring(tokenEndIndex); // Trim away
}
else
{
// No tokens, just add the text
sections.Add(value);
value = null;
}
}

return sections.ToArray();
}

private static void RichTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
string value = e.NewValue as string;
TextBlock textBlock = sender as TextBlock;
if (textBlock != null) textBlock.Inlines.Add(Traverse(value));
}
}
}

Property Listener


using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace RichTextBlockExample
{
public class PropertyListener
{
/// <summary>
/// Listens for changes to dependency properties on a given FrameworkElement calls a callback on property change.
/// Usage example: PropertyListener.ListenForChange("IsBusy", BusyIndicator, (d,e) => DoStuff())
/// (may cause memory leaks)
/// </summary>
public void ListenForChange(string propertyName, FrameworkElement element, PropertyChangedCallback callback)
{
var b = new Binding(propertyName) { Source = element };
var prop = DependencyProperty.RegisterAttached(
"ListenAttached" + propertyName,
typeof(object),
typeof(Control),
new PropertyMetadata(callback));
element.SetBinding(prop, b);
}
}
}

Thursday, 21 November 2013

Binding visual state manager to a view model

Using an attached property:

using System;
using System.Windows;
using System.Windows.Controls;

namespace StateHelperSL
{
public class VisualStateHelper : DependencyObject
{
public static readonly DependencyProperty VisualStatePropertyProperty = DependencyProperty.RegisterAttached(
"VisualStateProperty",
typeof(string),
typeof(VisualStateHelper),
new PropertyMetadata(VisualStatePropertyChangedCallback));

private static void VisualStatePropertyChangedCallback(DependencyObject s, DependencyPropertyChangedEventArgs e)
{
var ctrl = s as Control;
if (ctrl == null)
throw new InvalidOperationException("This attached property only supports types derived from Control.");

VisualStateManager.GoToState(ctrl, e.NewValue.ToString(), true);
}

public static string GetVisualStateProperty(DependencyObject obj)
{
return (string)obj.GetValue(VisualStatePropertyProperty);
}

public static void SetVisualStateProperty(DependencyObject obj, string value)
{
obj.SetValue(VisualStatePropertyProperty, value);
}
}
}
You can bind your view:
<UserControl x:Class="StateHelperSL.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:StateHelperSL"
local:VisualStateHelper.VisualStateProperty="{Binding State, Mode=OneWay}"
mc:Ignorable="d">

<Grid x:Name="LayoutRoot">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="VisualStateGroup">
<VisualState x:Name="Normal"/>
<VisualState x:Name="Gone">
<Storyboard>
<DoubleAnimation To="1000"
Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateY)"
Storyboard.TargetName="AnimatableButton"
d:IsOptimized="True"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<StackPanel Margin="50">
<Button x:Name="AnimatableButton" Click="ButtonBase_OnClick" Height="30" Content="Click me">
<Button.RenderTransform>
<CompositeTransform/>
</Button.RenderTransform>
</Button>

</StackPanel>
</Grid>
</UserControl>
View code behind:
using System.Windows;

namespace StateHelperSL
{
public partial class MainPage
{
private readonly ViewModel vm;

public MainPage()
{
this.InitializeComponent();
this.vm = new ViewModel();
this.DataContext = this.vm;
}

private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
vm.State = "Gone";
}
}
}

To your view model:


namespace StateHelperSL
{
public class ViewModel : PropertyChangedBase
{
private string state;

public ViewModel()
{
State = "Normal";
}

public string State
{
get { return this.state; }
set
{
if (state == value) return;
this.state = value;
NotifyOfPropertyChange("State");
}
}
}
}

Saturday, 2 November 2013

Fluid Dialog Wizard for Silverlight

My Fluid Dialog Wizard is a copy from Visual Studio 2012 setup dialog:

Features plenty of cool stuff: my Modern UI tab control, plenty of visual state manager examples with storyboards, animations, render transforms and transitions, behaviours, draggable dialog, styles, a clip attached property (that forces a panel to clip it’s contents),

Source:    https://github.com/stevenh77/FluidDialogWizard

Monday, 18 June 2012

Silverlight ContentMargin Attached Property

Here is a useful Silverlight attached property taken from a Xaml presentation by Miguel Castro.

By setting a property on a Panel it automatically sets the margin property for all its children.

<UserControl x:Class="AttachedProperty.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:b="clr-namespace:AttachedProperty">
<Grid x:Name="LayoutRoot" Background="White">
<StackPanel b:ContentMargin.Margin="10,5,20,15">
<TextBlock HorizontalAlignment="Center" Text="Stack Panel" />
<Button>Button #1</Button>
<Button>Button #2</Button>
<Button>Button #3</Button>
<Button>Button #4</Button>
<Button>Button #5</Button>
<Button>Button #6</Button>
</StackPanel>
</Grid>
</UserControl>


ContentMargin

using System.Windows;
using System.Windows.Controls;

namespace AttachedProperty
{
public static class ContentMargin
{
public static readonly DependencyProperty MarginProperty =
DependencyProperty.RegisterAttached("MarginValue",
typeof (Thickness),
typeof (ContentMargin),
new PropertyMetadata(default(Thickness), OnMarginChanged));

public static Thickness GetMargin(DependencyObject obj)
{
return (Thickness)obj.GetValue(MarginProperty);
}

public static void SetMargin(DependencyObject obj, Thickness value)
{
obj.SetValue(MarginProperty, value);
}

private static void OnMarginChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var element = obj as FrameworkElement;
if (element == null) return;

element.Loaded += element_Loaded;
}

static void element_Loaded(object sender, RoutedEventArgs e)
{
var panel = sender as Panel;
if (panel == null) return;

foreach (var child in panel.Children)
{
var element = child as FrameworkElement;
if (element == null) continue;
Thickness parentMargin = GetMargin(panel);
element.Margin = new Thickness(parentMargin.Left,
parentMargin.Top,
parentMargin.Right,
parentMargin.Bottom);
}
}
}
}


Source:  https://github.com/stevenh77/AttachedProperty