How to log name/path of clicked control in WPF? - wpf

I use the following code to log every click in our WinForms application. In essence it looks up a control from its HWND and then prints the types and names of the control and all of its parents. Something like MainForm"myWindow">TabPanel"mainTab">Button"save"
internal class ClickLogger : IMessageFilter
{
private const int WM_LBUTTONDOWN = 0x0201;
private const int WM_LBUTTONDBLCLK = 0x0203;
private const int WM_RBUTTONDOWN = 0x0204;
private const int MaxRecurseDepth = 30;
private readonly ILogger _log;
public ClickLogger(ILogger logger)
{
_log = logger;
}
[DebuggerStepThrough]
public bool PreFilterMessage(ref Message message)
{
if (message.Msg == WM_LBUTTONDOWN
|| message.Msg == WM_RBUTTONDOWN
|| message.Msg == WM_LBUTTONDBLCLK)
{
string path = "Unknown";
Control ctl = Control.FromHandle(message.HWnd);
if (ctl != null)
{
path = PathFromControl(ctl, MaxRecurseDepth).ToString();
}
string logEntry = string.Format("{0} Click on {1}",
WndMsgToClickName(message.Msg), path);
if (_log.IsInfoEnabled)
{
_log.Info(logEntry);
}
}
return false;
}
private StringBuilder PathFromControl(Control control, int maxDepth)
{
if(maxDepth == 0)
{
_log.Warn("Max recursion {0} reached whilst resolving path of control", MaxRecurseDepth);
return new StringBuilder("ERR");
}
string name = control.GetType().Name;
if (control.Name.IsNotBlank())
{
name = name + "\"" + control.Name + "\"";
}
if (control.Parent != null && control.Parent != control)
{
return PathFromControl(control.Parent, maxDepth - 1).Append(">").Append(name);
}
return new StringBuilder(name);
}
public void Initialize()
{
Application.AddMessageFilter(this);
}
private static string WndMsgToClickName(int msgId)
{
switch (msgId)
{
case WM_LBUTTONDOWN:
return "Left";
case WM_LBUTTONDBLCLK:
return "Double";
case WM_RBUTTONDOWN:
return "Right";
default:
return "0x" + Convert.ToString(msgId, 16);
}
}
}
Recently we've started to mix WPF and WinForms and the above click logger simply prints "Unknown" for any click on a WPF control.
Is there a way I can perform a similar trick for WPF controls? A method that would work across technologies would be great.

well, it doesn't exactly work across technologies but for wpf you can use a combination of this to get the clicks and any of the helpers in this question to cycle through the parents to get the path.

Related

Bug with JavaFX ComboBox?

One of my developers has attempted to extend ComboBox to autofilter based on what the user types:
public class AutoCompleteComboBox<T> extends ComboBox<T> {
private FilteredList<T> filteredItems;
private SortedList<T> sortedItems;
public AutoCompleteComboBox() {
setEditable(true);
setOnKeyReleased(e -> handleOnKeyReleasedEvent(e));
setOnMouseClicked(e -> handleOnMouseClicked(e));
}
private void handleOnMouseClicked(MouseEvent e) {
getItems().stream()
.filter(item -> item.toString().equals(getEditor().getText()))
.forEach(item -> getSelectionModel().select(item));
setCaretPositionToEnd();
}
private void handleOnKeyReleasedEvent(KeyEvent e) {
if (e.getCode() == KeyCode.UP || e.getCode() == KeyCode.DOWN) {
getItems().stream()
.filter(item -> item.toString().equals(getEditor().getText()))
.forEach(item -> getSelectionModel().select(item));
show();
setCaretPositionToEnd();
} else if (e.getCode() == KeyCode.ENTER || e.getCode() == KeyCode.TAB) {
String editorStr = getEditor().getText();
getSelectionModel().clearSelection();
getEditor().setText(editorStr);
setItems(this.sortedItems);
getItems().stream()
.filter(item -> item.toString().equals(editorStr))
.forEach(item -> getSelectionModel().select(item));
getEditor().selectEnd();
if (e.getCode() == KeyCode.ENTER) {
getEditor().deselect();
}
hide();
} else if (e.getText().length() == 1) {
getSelectionModel().clearSelection();
if (getEditor().getText().length() == 0) {
getEditor().setText(e.getText());
}
filterSelectionList();
show();
} else if (e.getCode() == KeyCode.BACK_SPACE && getEditor().getText().length() > 0) {
String editorStr = getEditor().getText();
getSelectionModel().clearSelection();
getEditor().setText(editorStr);
int beforeFilter = getItems().size();
filterSelectionList();
int afterFilter = getItems().size();
if (afterFilter > beforeFilter) {
hide();
}
show();
} else if (e.getCode() == KeyCode.BACK_SPACE && getEditor().getText().length() == 0) {
clearSelection();
hide();
show();
}
}
private void filterSelectionList() {
setFilteredItems();
setCaretPositionToEnd();
}
private void setFilteredItems() {
filteredItems.setPredicate(item ->
item.toString().toLowerCase().startsWith(getEditor().getText().toLowerCase()));
}
private void setCaretPositionToEnd() {
getEditor().selectEnd();
getEditor().deselect();
}
public void setInitItems(ObservableList<T> values) {
filteredItems = new FilteredList<>(values);
sortedItems = new SortedList<>(filteredItems);
setItems(this.sortedItems);
}
public void clearSelection() {
getSelectionModel().clearSelection();
getEditor().clear();
if (this.filteredItems != null) {
this.filteredItems.setPredicate(item -> true);
}
}
public T getSelectedItem() {
T selectedItem = null;
if (getSelectionModel().getSelectedIndex() > -1) {
selectedItem = getItems().get(getSelectionModel().getSelectedIndex());
}
return selectedItem;
}
public void select(String value) {
if (!value.isEmpty()) {
getItems().stream()
.filter(item -> value.equals(item.toString()))
.findFirst()
.ifPresent(item -> getSelectionModel().select(item));
}
}
}
The control works just fine, until I bind something to either the Selected or Value property. Once that happens, once I attempt to leave the field (or hit enter which causes the action to occur), I get the following exception. (Simply selecting the value with the mouse selects without issues) What I am binding to in this control is the simple object of CodeTableValue - which has two properties - both Strings, Code & Value. The toString returns the Value property.
If the control is just set up without any listeners, it works just fine. But as soon as I listen to one of the properties for the value it fails.
When I use the AutoCompleteComboBox control in my class:
#FXML private AutoCompleteComboBox<CodeTableValue> myAutoCompleteBox;
myAutoCompleteBox.getSelectionModel().selectedItemProperty().addListener((obs, ov, nv) -> {
System.out.println(nv);
});
The exception that is thrown:
Exception in thread "JavaFX Application Thread" java.lang.ClassCastException: java.lang.String cannot be cast to cache.CodeTableValue
at com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:361)
at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
at javafx.beans.property.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:74)
at javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:102)
at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:146)
at javafx.scene.control.SelectionModel.setSelectedItem(SelectionModel.java:102)
at javafx.scene.control.ComboBox.lambda$new$152(ComboBox.java:249)
at com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:361)
at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
at javafx.beans.property.ObjectPropertyBase.fireValueChangedEvent(ObjectPropertyBase.java:105)
at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:146)
at javafx.scene.control.ComboBoxBase.setValue(ComboBoxBase.java:150)
at com.sun.javafx.scene.control.skin.ComboBoxPopupControl.setTextFromTextFieldIntoComboBoxValue(ComboBoxPopupControl.java:405)
at com.sun.javafx.scene.control.skin.ComboBoxPopupControl.lambda$new$291(ComboBoxPopupControl.java:82)
at com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:361)
at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
at javafx.beans.property.ReadOnlyBooleanPropertyBase.fireValueChangedEvent(ReadOnlyBooleanPropertyBase.java:72)
at javafx.scene.Node$FocusedProperty.notifyListeners(Node.java:7718)
at javafx.scene.Node.setFocused(Node.java:7771)
at javafx.scene.Scene$KeyHandler.setWindowFocused(Scene.java:3932)
at javafx.scene.Scene$KeyHandler.lambda$new$11(Scene.java:3954)
at com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:137)
at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
at javafx.beans.property.ReadOnlyBooleanPropertyBase.fireValueChangedEvent(ReadOnlyBooleanPropertyBase.java:72)
at javafx.beans.property.ReadOnlyBooleanWrapper.fireValueChangedEvent(ReadOnlyBooleanWrapper.java:103)
at javafx.beans.property.BooleanPropertyBase.markInvalid(BooleanPropertyBase.java:110)
at javafx.beans.property.BooleanPropertyBase.set(BooleanPropertyBase.java:144)
at javafx.stage.Window.setFocused(Window.java:439)
at com.sun.javafx.stage.WindowPeerListener.changedFocused(WindowPeerListener.java:59)
at com.sun.javafx.tk.quantum.GlassWindowEventHandler.run(GlassWindowEventHandler.java:100)
at com.sun.javafx.tk.quantum.GlassWindowEventHandler.run(GlassWindowEventHandler.java:40)
at java.security.AccessController.doPrivileged(Native Method)
at com.sun.javafx.tk.quantum.GlassWindowEventHandler.lambda$handleWindowEvent$423(GlassWindowEventHandler.java:150)
at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:389)
at com.sun.javafx.tk.quantum.GlassWindowEventHandler.handleWindowEvent(GlassWindowEventHandler.java:148)
at com.sun.glass.ui.Window.handleWindowEvent(Window.java:1266)
at com.sun.glass.ui.Window.notifyFocus(Window.java:1245)
at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at com.sun.glass.ui.win.WinApplication.lambda$null$148(WinApplication.java:191)
at java.lang.Thread.run(Thread.java:745)
If the editableFlag is false, then this works. It just happens when we allow the user to type into the combo box to filter the content. Without the binding, we get no exceptions and the values are correctly set.
I have been digging deeper into this exception and it is being thrown here:
com.sun.javafx.binding.ExpressionHelper.Generic.fireValueChangedEvent()
if (curChangeSize > 0) {
final T oldValue = currentValue;
currentValue = observable.getValue();
final boolean changed = (currentValue == null)? (oldValue != null) : !currentValue.equals(oldValue);
if (changed) {
for (int i = 0; i < curChangeSize; i++) {
try {
curChangeList[i].changed(observable, oldValue, currentValue);
} catch (Exception e) {
Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
}
}
}
}
It comes from here only when the valueProperty or selectedItemProperty is bound. Otherwise we don't get this issue.
javafx.beans.property.ObjectPropertyBase.set(T) #Override
public void set(T newValue) {
if (isBound()) {
throw new java.lang.RuntimeException((getBean() != null && getName() != null ?
getBean().getClass().getSimpleName() + "." + getName() + " : ": "") + "A bound value cannot be set.");
}
if (value != newValue) {
value = newValue;
markInvalid();
}
}
I have attempted to add a StringConverter to the editor to see if that would correct the issue, but again, it continues to throw this exception. Perhaps we are attempting to handle this all wrong?
Basically we are looking to filter the combobox selection items as the user types into the field. If there is different way we should handle this please let me know, but at the moment, I think that this could be a bug in the JDK? If the field is not bound then we have no issues, but once bound, this exception occurs when the field either loses focus, or the enter key is pressed.
I've created a quick sample using your control:
#Override
public void start(Stage primaryStage) {
AutoCompleteComboBox<CodeTableValue> myAutoCompleteBox =
new AutoCompleteComboBox<>();
myAutoCompleteBox.setInitItems(FXCollections.observableArrayList(new CodeTableValue("One", "1"),
new CodeTableValue("Two", "2"), new CodeTableValue("Three", "4")));
StackPane root = new StackPane(myAutoCompleteBox);
myAutoCompleteBox.getSelectionModel().selectedItemProperty().addListener((obs, ov, nv) -> {
System.out.println(nv);
});
Scene scene = new Scene(root, 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
Based on a simple model class:
public class CodeTableValue {
private String code;
private String value;
public CodeTableValue(String code, String value) {
this.code = code;
this.value = value;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
#Override
public String toString() {
return "code=" + code + ", value=" + value;
}
}
I can reproduce your exception once I finish typing and click enter to commit the value and leave the control:
java.lang.ClassCastException: java.lang.String cannot be cast to CodeTableValue
The exception comes from the fact that the TextField used from the ComboBox in edit mode will return always a String, but you are forcing the custom ComboBox to use a CodeTableValue class.
The solution is just providing a way to convert between both String and CodeTableValue, using a StringConverter.
So I've modified your control:
public AutoCompleteComboBox(StringConverter<T> converter) {
setEditable(true);
setOnKeyReleased(e -> handleOnKeyReleasedEvent(e));
setOnMouseClicked(e -> handleOnMouseClicked(e));
super.setConverter(converter);
}
and now in the sample:
AutoCompleteComboBox<CodeTableValue> myAutoCompleteBox =
new AutoCompleteComboBox<>(new StringConverter<CodeTableValue>() {
#Override
public String toString(CodeTableValue object) {
if (object != null) {
return object.getValue();
}
return null;
}
#Override
public CodeTableValue fromString(String string) {
return new CodeTableValue(string, string);
}
});
This works now, without the ClassCastException.
Obviously you'll have to provide a way to type a string (either for code or for value) and retrieve the other out of it.

Query CRM data in Silverlight

I am building a silverlight app for CRM 2011 and I was wondering what the best way to retrieve data from the CRM system is.
I have linked in my organisation as a service reference and am able to access that. I have seen a few different ways to retrieve data but they all seem rather complicated. Is there anything like what we can use in plugins such as a fetch XMl query or a simple Service.Retrieve method?
Thanks
If you add a service reference to your project you can use LINQ to query the datasets.
You can download the CSDL from Developer Resources under the customisation area.
private FelineSoftContext context;
private System.String serverUrl;
private DataServiceCollection<SalesOrder> _orders;
public MainPage()
{
InitializeComponent();
serverUrl = (String)GetContext().Invoke("getServerUrl");
//Remove the trailing forward slash returned by CRM Online
//So that it is always consistent with CRM On Premises
if (serverUrl.EndsWith("/"))
serverUrl = serverUrl.Substring(0, serverUrl.Length - 1);
Uri ODataUri = new Uri(serverUrl + "/xrmservices/2011/organizationdata.svc/", UriKind.Absolute);
context = new FelineSoftContext(ODataUri) { IgnoreMissingProperties = true };
var orders = from ord in context.SalesOrderSet
orderby ord.Name
select new SalesOrder
{
Name = ord.Name,
SalesOrderId = ord.SalesOrderId
};
_orders = new DataServiceCollection<SalesOrder>();
_orders.LoadCompleted += _orders_LoadCompleted;
_orders.LoadAsync(orders);
}
void _orders_LoadCompleted(object sender, LoadCompletedEventArgs e)
{
if (e.Error == null)
{
if (_orders.Continuation != null)
{
_orders.LoadNextPartialSetAsync();
}
else
{
OrderLookup.ItemsSource = _orders;
OrderLookup.DisplayMemberPath = "Name";
OrderLookup.SelectedValuePath = "Id";
}
}
}
You will also need to add a method in your class as below:
private static ScriptObject GetContext()
{
ScriptObject xrmProperty = (ScriptObject)HtmlPage.Window.GetProperty("Xrm");
if (null == xrmProperty)
{
//It may be that the global context should be used
try
{
ScriptObject globalContext = (ScriptObject)HtmlPage.Window.Invoke("GetGlobalContext");
return globalContext;
}
catch (System.InvalidOperationException)
{
throw new InvalidOperationException("Property \"Xrm\" is null and the Global Context is not available.");
}
}
ScriptObject pageProperty = (ScriptObject)xrmProperty.GetProperty("Page");
if (null == pageProperty)
{
throw new InvalidOperationException("Property \"Xrm.Page\" is null");
}
ScriptObject contextProperty = (ScriptObject)pageProperty.GetProperty("context");
if (null == contextProperty)
{
throw new InvalidOperationException("Property \"Xrm.Page.context\" is null");
}
return contextProperty;
}
You will need to add an additional class library and put the following code within it. Change the class name to match the Context you exported:
partial class FelineSoftContext
{
#region Methods
partial void OnContextCreated()
{
this.ReadingEntity += this.OnReadingEntity;
this.WritingEntity += this.OnWritingEntity;
}
#endregion
#region Event Handlers
private void OnReadingEntity(object sender, ReadingWritingEntityEventArgs e)
{
ODataEntity entity = e.Entity as ODataEntity;
if (null == entity)
{
return;
}
entity.ClearChangedProperties();
}
private void OnWritingEntity(object sender, ReadingWritingEntityEventArgs e)
{
ODataEntity entity = e.Entity as ODataEntity;
if (null == entity)
{
return;
}
entity.RemoveUnchangedProperties(e.Data);
entity.ClearChangedProperties();
}
#endregion
}
public abstract class ODataEntity
{
private readonly Collection<string> ChangedProperties = new Collection<string>();
public ODataEntity()
{
EventInfo info = this.GetType().GetEvent("PropertyChanged");
if (null != info)
{
PropertyChangedEventHandler method = new PropertyChangedEventHandler(this.OnEntityPropertyChanged);
//Ensure that the method is not attached and reattach it
info.RemoveEventHandler(this, method);
info.AddEventHandler(this, method);
}
}
#region Methods
public void ClearChangedProperties()
{
this.ChangedProperties.Clear();
}
internal void RemoveUnchangedProperties(XElement element)
{
const string AtomNamespace = "http://www.w3.org/2005/Atom";
const string DataServicesNamespace = "http://schemas.microsoft.com/ado/2007/08/dataservices";
const string DataServicesMetadataNamespace = DataServicesNamespace + "/metadata";
if (null == element)
{
throw new ArgumentNullException("element");
}
List<XElement> properties = (from c in element.Elements(XName.Get("content", AtomNamespace)
).Elements(XName.Get("properties", DataServicesMetadataNamespace)).Elements()
select c).ToList();
foreach (XElement property in properties)
{
if (!this.ChangedProperties.Contains(property.Name.LocalName))
{
property.Remove();
}
}
}
private void OnEntityPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (!this.ChangedProperties.Contains(e.PropertyName))
{
this.ChangedProperties.Add(e.PropertyName);
}
}
#endregion
}
I am will suggest you to use Silvercrmsoap , it's very easy to use. I have used this in my silverlight projects.

WPF Page navigation

I use a custom textblock in my WPF Application, when I use it in WPF Windows it worked good but when I use it in a WPF Page it make a problem. When I click on a link in my Custom Control it browse the link and show in browser but the WPF page navigate back to another WPF Page too (first page)
namespace Dtwitter.Controls
{
public class TweetTextBlock : TextBlock
{
public TweetTextBlock()
{
}
#region Dependency properties
public string TweetText
{
get { return (string)GetValue(TweetTextProperty); }
set { SetValue(TweetTextProperty, value); }
}
// Using a DependencyProperty as the backing store for TweetText. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TweetTextProperty =
DependencyProperty.Register("TweetText", typeof(string), typeof(TweetTextBlock),
new FrameworkPropertyMetadata(string.Empty, new PropertyChangedCallback(OnTweetTextChanged)));
#endregion
private static void OnTweetTextChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
string text = args.NewValue as string;
if (!string.IsNullOrEmpty(text))
{
TweetTextBlock textblock = (TweetTextBlock)obj;
textblock.Inlines.Clear();
textblock.Inlines.Add(" ");
string[] words = Regex.Split(text, #"([ \(\)\{\}\[\]])");
string possibleUserName = words[0].ToString();
if ((possibleUserName.Length > 1) && (possibleUserName.Substring(1, 1) == "#"))
{
textblock = FormatName(textblock, possibleUserName);
words.SetValue("", 0);
}
foreach (string word in words)
{
// clickable hyperlinks
if (UrlShorteningService.IsUrl(word))
{
try
{
Hyperlink link = new Hyperlink();
link.NavigateUri = new Uri(word);
link.Inlines.Add(word);
link.Click += new RoutedEventHandler(link_Click);
link.ToolTip = "Open link in the default browser";
textblock.Inlines.Add(link);
}
catch
{
//TODO:What are we catching here? Why? Log it?
textblock.Inlines.Add(word);
}
}
// clickable #name
else if (word.StartsWith("#"))
{
textblock = FormatName(textblock, word);
}
// clickable #hashtag
else if (word.StartsWith("#"))
{
string hashtag = String.Empty;
Match foundHashtag = Regex.Match(word, #"#(\w+)(?<suffix>.*)");
if (foundHashtag.Success)
{
hashtag = foundHashtag.Groups[1].Captures[0].Value;
Hyperlink tag = new Hyperlink();
tag.Inlines.Add(hashtag);
string hashtagUrl = "http://search.twitter.com/search?q=%23{0}";
// The main application has access to the Settings class, where a
// user-defined hashtagUrl can be stored. This hardcoded one that
// is used to set the NavigateUri is just a default behavior that
// will be used if the click event is not handled for some reason.
tag.NavigateUri = new Uri(String.Format(hashtagUrl, hashtag));
tag.ToolTip = "Show statuses that include this hashtag";
tag.Tag = hashtag;
tag.Click += new RoutedEventHandler(hashtag_Click);
textblock.Inlines.Add("#");
textblock.Inlines.Add(tag);
textblock.Inlines.Add(foundHashtag.Groups["suffix"].Captures[0].Value);
}
}
else
{
textblock.Inlines.Add(word);
}
}
textblock.Inlines.Add(" ");
}
}
public static TweetTextBlock FormatName(TweetTextBlock textblock, string word)
{
string userName = String.Empty;
string firstLetter = word.Substring(0, 1);
Match foundUsername = Regex.Match(word, #"#(\w+)(?<suffix>.*)");
if (foundUsername.Success)
{
userName = foundUsername.Groups[1].Captures[0].Value;
Hyperlink name = new Hyperlink();
name.Inlines.Add(userName);
name.NavigateUri = new Uri("http://twitter.com/" + userName);
name.ToolTip = "View #" + userName + "'s recent tweets";
name.Tag = userName;
name.Click += new RoutedEventHandler(name_Click);
if (firstLetter != "#")
textblock.Inlines.Add(firstLetter);
textblock.Inlines.Add("#");
textblock.Inlines.Add(name);
textblock.Inlines.Add(foundUsername.Groups["suffix"].Captures[0].Value);
}
return textblock;
}
static void link_Click(object sender, RoutedEventArgs e)
{
try
{
System.Diagnostics.Process.Start(((Hyperlink)sender).NavigateUri.ToString());
}
catch
{
//TODO: Log specific URL that caused error
MessageBox.Show("There was a problem launching the specified URL.", "Error", MessageBoxButton.OK, MessageBoxImage.Exclamation);
}
}
}
}
change your link click method to
static void link_click(Object sender, RequestNavigateEventArgs e) {
try {
System.Diagnostics.Process.Start(e.Uri.ToString());
} catch {
//TODO: Log specific URL that caused error
MessageBox.Show("There was a problem launching the specified URL.", "Error", MessageBoxButton.OK, MessageBoxImage.Exclamation);
} finally {
e.Handled = true;
}
}
change your
link.Click+=new RoutedEventHandler(link_Click);
to
link.RequestNavigate+=new RequestNavigateEventHandler(link_Click);
Set e.Handled=true in link_click to mark you've dealt with the link click to prevent the framework from additionally processing your link click further.
Alternatively you may be able to just set the TargetName property of Hyperlink to "_blank" and not need the process start command
The code below should make it work the same way in both cases (Page and Window)....
try this to open the hyperlink in web browser in MouseDown of the Hyperlink object.
Process.Start((e.OriginalSource as Hyperlink).NavigateUri.ToString());
e.Handled = true;
Let me know if this helps.

How to bind a TextBlock to a resource containing formatted text?

I have a TextBlock in my WPF window.
<TextBlock>
Some <Bold>formatted</Bold> text.
</TextBlock>
When it is rendered it looks like this,
Some formatted text.
My question is, can I bind this inline "content" to a resource in my application?
I got as far as:
Making an application resource string,
myText="Some <Bold>formatted</Bold> text."
and the following xaml (Some code omitted for brevity)
<Window xmlns:props="clr-namespace:MyApp.Properties">
<Window.Resources>
<props:Resources x:Key="Resources"/>
</Window.Resources>
<TextBlock x:Name="Try1"
Text="{Binding Source={StaticResource Resources} Path=myText}"/>
<TextBlock x:Name="Try2">
<Binding Source="{StaticResource Resources}" Path="myText" />
</TextBlock>
</Window>
Try1 renders with the tags in place and not effecting formatting.
Some <Bold>formatted<Bold> text.
Try2 will not compile or render because the resource "myText" is not of type Inline but a string.
Is this seemingly simple task possible and if so how?
Here is my modified code for recursively format text. It handles Bold, Italic, Underline and LineBreak but can easily be extended to support more (modify the switch statement).
public static class MyBehavior
{
public static string GetFormattedText(DependencyObject obj)
{
return (string)obj.GetValue(FormattedTextProperty);
}
public static void SetFormattedText(DependencyObject obj, string value)
{
obj.SetValue(FormattedTextProperty, value);
}
public static readonly DependencyProperty FormattedTextProperty =
DependencyProperty.RegisterAttached("FormattedText",
typeof(string),
typeof(MyBehavior),
new UIPropertyMetadata("", FormattedTextChanged));
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)
{
case "<Bold>":
return new Bold(Traverse(content));
case "<Italic>":
return new Italic(Traverse(content));
case "<Underline>":
return new Underline(Traverse(content));
case "<LineBreak/>":
return new LineBreak();
default:
return new Run(section);
}
}
else return new Run(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>
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>
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();
}
private static void FormattedTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
string value = e.NewValue as string;
TextBlock textBlock = sender as TextBlock;
if (textBlock != null)
textBlock.Inlines.Add(Traverse(value));
}
}
Edit: (proposed by Spook)
A shorter version, but requires the text to be XML-valid:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Xml;
// (...)
public static class TextBlockHelper
{
#region FormattedText Attached dependency property
public static string GetFormattedText(DependencyObject obj)
{
return (string)obj.GetValue(FormattedTextProperty);
}
public static void SetFormattedText(DependencyObject obj, string value)
{
obj.SetValue(FormattedTextProperty, value);
}
public static readonly DependencyProperty FormattedTextProperty =
DependencyProperty.RegisterAttached("FormattedText",
typeof(string),
typeof(TextBlockHelper),
new UIPropertyMetadata("", FormattedTextChanged));
private static void FormattedTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
string value = e.NewValue as string;
TextBlock textBlock = sender as TextBlock;
if (textBlock != null)
{
textBlock.Inlines.Clear();
textBlock.Inlines.Add(Process(value));
}
}
#endregion
static Inline Process(string value)
{
XmlDocument doc = new XmlDocument();
doc.LoadXml(value);
Span span = new Span();
InternalProcess(span, doc.ChildNodes[0]);
return span;
}
private static void InternalProcess(Span span, XmlNode xmlNode)
{
foreach (XmlNode child in xmlNode)
{
if (child is XmlText)
{
span.Inlines.Add(new Run(child.InnerText));
}
else if (child is XmlElement)
{
Span spanItem = new Span();
InternalProcess(spanItem, child);
switch (child.Name.ToUpper())
{
case "B":
case "BOLD":
Bold bold = new Bold(spanItem);
span.Inlines.Add(bold);
break;
case "I":
case "ITALIC":
Italic italic = new Italic(spanItem);
span.Inlines.Add(italic);
break;
case "U":
case "UNDERLINE":
Underline underline = new Underline(spanItem);
span.Inlines.Add(underline);
break;
}
}
}
}
}
And an example of usage:
<RootItem xmlns:u="clr-namespace:MyApp.Helpers">
<TextBlock u:TextBlockHelper.FormattedText="{Binding SomeProperty}" />
</RootItem>
I've added hyperlink and image support to Vincents solution:
public static class FormattedTextBlock
{
public static string GetFormattedText(DependencyObject obj)
{
return (string)obj.GetValue(FormattedTextProperty);
}
public static void SetFormattedText(DependencyObject obj, string value)
{
obj.SetValue(FormattedTextProperty, value);
}
public static readonly DependencyProperty FormattedTextProperty =
DependencyProperty.RegisterAttached("FormattedText",
typeof(string),
typeof(FormattedTextBlock),
new UIPropertyMetadata("", FormattedTextChanged));
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.ToUpper())
{
case "<B>":
case "<BOLD>":
/* <b>Bold text</b> */
return new Bold(Traverse(content));
case "<I>":
case "<ITALIC>":
/* <i>Italic text</i> */
return new Italic(Traverse(content));
case "<U>":
case "<UNDERLINE>":
/* <u>Underlined text</u> */
return new Underline(Traverse(content));
case "<BR>":
case "<BR/>":
case "<LINEBREAK/>":
/* Line 1<br/>line 2 */
return new LineBreak();
case "<A>":
case "<LINK>":
/* <a>{http://www.google.de}Google</a> */
var start = content.IndexOf("{");
var end = content.IndexOf("}");
var url = content.Substring(start + 1, end - 1);
var text = content.Substring(end + 1);
var link = new Hyperlink();
link.NavigateUri = new System.Uri(url);
link.RequestNavigate += Hyperlink_RequestNavigate;
link.Inlines.Add(text);
return link;
case "<IMG>":
case "<IMAGE>":
/* <image>pack://application:,,,/ProjectName;component/directory1/directory2/image.png</image> */
var image = new Image();
var bitmap = new BitmapImage(new Uri(content));
image.Source = bitmap;
image.Width = bitmap.Width;
image.Height = bitmap.Height;
var container = new InlineUIContainer();
container.Child = image;
return container;
default:
return new Run(section);
}
}
else return new Run(section);
}
else // Group together
{
Span span = new Span();
foreach(string section in sections)
span.Inlines.Add(Traverse(section));
return span;
}
}
static void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
{
Process.Start(e.Uri.ToString());
}
/// <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>
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>
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();
}
private static void FormattedTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
string value = e.NewValue as string;
TextBlock textBlock = sender as TextBlock;
if(textBlock != null)
textBlock.Inlines.Add(Traverse(value));
}
}
Thanks Vincent for the great template, it works like a charm!
How about using attached behavior? Below code only handles bold tags. Each word which should be bold needs to be wrapped in bold tags. You probably want to make the class accept other formats as well. Also spaces needs to be handled better, the class strips out consecutive spaces and add one extra to the end. So consider below class as demo code only which will need further work to be useful but it should get you started.
XAML:
<Window x:Class="FormatTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:FormatTest="clr-namespace:FormatTest"
Title="Window1" Height="300" Width="300">
<TextBlock FormatTest:FormattedTextBehavior.FormattedText="{Binding Path=Text}" />
</Window>
Code behind:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
namespace FormatTest
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = this;
}
public string Text { get { return "Some <Bold>formatted</Bold> text."; } }
}
public static class FormattedTextBehavior
{
public static string GetFormattedText(DependencyObject obj)
{
return (string)obj.GetValue(FormattedTextProperty);
}
public static void SetFormattedText(DependencyObject obj, string value)
{
obj.SetValue(FormattedTextProperty, value);
}
public static readonly DependencyProperty FormattedTextProperty =
DependencyProperty.RegisterAttached("FormattedText",
typeof(string),
typeof(FormattedTextBehavior),
new UIPropertyMetadata("", FormattedTextChanged));
private static void FormattedTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
TextBlock textBlock = sender as TextBlock;
string value = e.NewValue as string;
string[] tokens = value.Split(' ');
foreach (string token in tokens)
{
if (token.StartsWith("<Bold>") && token.EndsWith("</Bold>"))
{
textBlock.Inlines.Add(new Bold(new Run(token.Replace("<Bold>", "").Replace("</Bold>", "") + " ")));
}
else
{
textBlock.Inlines.Add(new Run(token + " "));
}
}
}
}
}
EDIT:
This line,
<props:Resources x:Key="Resources"/>
is a bad approach to accesing the Project.Properties.Resources namespace. It causes awkward glitches when recompiling.
Much better to use x:Static to do somthing like this,
Text="{x:Static props:Resources.SomeText}"
in your binding. Thx to Ben
Okay, this is how I did it. It's not perfect but it works.
Remember, there is a project resource called FormattedText.
cs:
// TextBlock with a bindable InlineCollection property.
// Type is List(Inline) not InlineCollection becuase
// InlineCollection makes the IDE xaml parser complain
// presumably this is caused by an inherited attribute.
public class BindableTextBlock : TextBlock
{
public static readonly DependencyProperty InlineCollectionProperty =
DependencyProperty.Register(
"InlineCollection",
typeof(List<Inline>),
typeof(BindableTextBlock),
new UIPropertyMetadata(OnInlineCollectionChanged));
private static void OnInlineCollectionChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
BinableTextBlock instance = sender as BindableTextBlock;
if (instance != null)
{
List<Inline> newText = e.NewValue as List<Inline>;
if (newText != null)
{
// Clear the underlying Inlines property
instance.Inlines.Clear();
// Add the passed List<Inline> to the real Inlines
instance.Inlines.AddRange(newText.ToList());
}
}
}
public List<Inline> InlineCollection
{
get
{
return (List<Inline>)GetValue(InlineCollectionProperty);
}
set
{
SetValue(InlineCollectionProperty, value);
}
}
}
// Convertor between a string of xaml with implied run elements
// and a generic list of inlines
[ValueConversion(typeof(string), typeof(List<Inline>))]
public class StringInlineCollectionConvertor : IValueConverter
{
public object Convert(object value,
Type targetType,
object parameter,
System.Globalization.CultureInfo culture)
{
string text = value as String;
// a surrogate TextBlock to host an InlineCollection
TextBlock results = new TextBlock();
if (!String.IsNullOrEmpty(text))
{
//Arbritary literal acting as a replace token,
//must not exist in the empty xaml definition.
const string Replace = "xxx";
// add a dummy run element and replace it with the text
results.Inlines.Add(new Run(Replace));
string resultsXaml = XamlWriter.Save(results);
string resultsXamlWithText = resultsXaml.Replace(Replace, text);
// deserialise the xaml back into our TextBlock
results = XamlReader.Parse(resultsXamlWithText) as TextBlock;
}
return results.Inlines.ToList<Inline>();
}
// Not clear when this will be called but included for completeness
public object ConvertBack(
object value,
Type targetType,
object parameter,
System.Globalization.CultureInfo culture)
{
String results = String.Empty;
InlineCollection inlines = value as InlineCollection;
if (inlines != null)
{
//read the xaml as xml and return the "content"
var reader =
XElement.Parse(XamlWriter.Save(inlines)).CreateReader();
reader.MoveToContent();
results = reader.ReadInnerXml();
}
return results;
}
}
xaml:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:props="clr-namespace:Project.Properties"
xmlns:local="clr-namespace:Project">
<Window.Resources>
<props:Resources x:Key="Resources"/>
<local:StringInlineCollectionConvertor x:Key="InlineConvert"/>
</Window.Resources>
<local:BindableTextBlock InlineCollection="
{Binding Source={StaticResource Resources},
Path=FormattedText,
Converter={StaticResource InlineConvert}}"/>
</Window>
I made 2 classes. A sub-classed TextBlock with a "bindable" InlineCollection and an IValueConverter to convert the collection from and to a String.
Using InlineCollection directly as the type of the property made VS2010 complain, although the code still ran fine. I changed to a generic list of Inlines. I assume that there is an inherited attribute telling VS that the InlineCollection has no constructor.
I tryed making the InlineCollection property the BindableTextBlock's ContentProperty but ran into issues and out of time. Please feel free to take the next step and tell me about it.
I apologise for any errata but this code had to be transcribed and sanitised.
If there is a better way of doing this, surely there must be, please tell me that too. Wouldn't it be nice if this functionality was built in or, have I missed something?
I ended up needing to do this in my application and had to support many of the markup possible normally in TextBlock inlines, so I took Wallstreet Programmer's answer above (which works beautifully and is much less complicated than most other answers I found on this topic) and expanded on it. I figure someone else might find this useful.
I haven't thoroughly tested this with ALL the tags yet, but every one I've tested has worked like a charm. I also suspect it's not the fastest code in the world, but my own testing with several thousand formatted messages in a ListView seemed surprisingly zippy. YMMV. Code is below:
XAML:
<Window x:Class="FormatTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:FormatTest="clr-namespace:FormatTest"
Title="Window1" Height="300" Width="300">
<TextBlock FormatTest:FormattedTextBehavior.FormattedText="{Binding Path=Text}" />
</Window>
C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
namespace FormatTest
{
public static class FormattedTextBehavior
{
public class TextPart
{
public String mType = String.Empty;
public Inline mInline = null;
public InlineCollection mChildren = null;
public TextPart() {}
public TextPart(String t, Inline inline, InlineCollection col)
{
mType = t;
mInline = inline;
mChildren = col;
}
}
private static Regex mRegex = new Regex(#"<(?<Span>/?[^>]*)>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static Regex mSpanRegex = new Regex("(?<Key>[^\\s=]+)=\"(?<Val>[^\\s\"]*)\"", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static string GetFormattedText(DependencyObject obj)
{
return (string)obj.GetValue(FormattedTextProperty);
}
public static void SetFormattedText(DependencyObject obj, string value)
{
obj.SetValue(FormattedTextProperty, value);
}
public static readonly DependencyProperty FormattedTextProperty =
DependencyProperty.RegisterAttached("FormattedText",
typeof(string),
typeof(FormattedTextBehavior),
new UIPropertyMetadata("", FormattedTextChanged));
private static void FormattedTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
TextBlock textBlock = sender as TextBlock;
FormatText(e.NewValue as string, new TextPart("TextBlock", null, textBlock.Inlines));
}
public static void FormatText(String s, TextPart root)
{
int len = s.Length;
int lastIdx = 0;
List<TextPart> parts = new List<TextPart>();
parts.Add(root);
Match m = mRegex.Match(s);
while (m.Success)
{
String tag = m.Result("${Span}");
if (tag.StartsWith("/"))
{
String prevStr = s.Substring(lastIdx, m.Index - lastIdx);
TextPart part = parts.Last();
if (!String.IsNullOrEmpty(prevStr))
{
if (part.mChildren != null)
{
part.mChildren.Add(new Run(prevStr));
}
else if (part.mInline is Run)
{
(part.mInline as Run).Text = prevStr;
}
}
if (!tag.Substring(1).Equals(part.mType, StringComparison.InvariantCultureIgnoreCase))
{
Logger.LogD("Mismatched End Tag '" + tag.Substring(1) + "' (expected </" + part.mType + ">) at position " + m.Index.ToString() + " in String '" + s + "'");
}
if (parts.Count > 1)
{
parts.RemoveAt(parts.Count - 1);
TextPart parentPart = parts.Last();
if (parentPart.mChildren != null)
{
parentPart.mChildren.Add(part.mInline);
}
}
}
else
{
TextPart prevPart = parts.Last();
String prevStr = s.Substring(lastIdx, m.Index - lastIdx);
if (!String.IsNullOrEmpty(prevStr))
{
if (prevPart.mChildren != null)
{
prevPart.mChildren.Add(new Run(prevStr));
}
else if (prevPart.mInline is Run)
{
(prevPart.mInline as Run).Text = prevStr;
}
}
bool hasAttributes = false;
TextPart part = new TextPart();
if (tag.StartsWith("bold", StringComparison.InvariantCultureIgnoreCase))
{
part.mType = "BOLD";
part.mInline = new Bold();
part.mChildren = (part.mInline as Bold).Inlines;
}
else if (tag.StartsWith("underline", StringComparison.InvariantCultureIgnoreCase))
{
part.mType = "UNDERLINE";
part.mInline = new Underline();
part.mChildren = (part.mInline as Underline).Inlines;
}
else if (tag.StartsWith("italic", StringComparison.InvariantCultureIgnoreCase))
{
part.mType = "ITALIC";
part.mInline = new Italic();
part.mChildren = (part.mInline as Italic).Inlines;
}
else if (tag.StartsWith("linebreak", StringComparison.InvariantCultureIgnoreCase))
{
part.mType = "LINEBREAK";
part.mInline = new LineBreak();
}
else if (tag.StartsWith("span", StringComparison.InvariantCultureIgnoreCase))
{
hasAttributes = true;
part.mType = "SPAN";
part.mInline = new Span();
part.mChildren = (part.mInline as Span).Inlines;
}
else if (tag.StartsWith("run", StringComparison.InvariantCultureIgnoreCase))
{
hasAttributes = true;
part.mType = "RUN";
part.mInline = new Run();
}
else if (tag.StartsWith("hyperlink", StringComparison.InvariantCultureIgnoreCase))
{
hasAttributes = true;
part.mType = "HYPERLINK";
part.mInline = new Hyperlink();
part.mChildren = (part.mInline as Hyperlink).Inlines;
}
if (hasAttributes && part.mInline != null)
{
Match m2 = mSpanRegex.Match(tag);
while (m2.Success)
{
String key = m2.Result("${Key}");
String val = m2.Result("${Val}");
if (key.Equals("FontWeight", StringComparison.InvariantCultureIgnoreCase))
{
FontWeight fw = FontWeights.Normal;
try
{
fw = (FontWeight)new FontWeightConverter().ConvertFromString(val);
}
catch (Exception)
{
fw = FontWeights.Normal;
}
part.mInline.FontWeight = fw;
}
else if (key.Equals("FontSize", StringComparison.InvariantCultureIgnoreCase))
{
double fs = part.mInline.FontSize;
if (Double.TryParse(val, out fs))
{
part.mInline.FontSize = fs;
}
}
else if (key.Equals("FontStretch", StringComparison.InvariantCultureIgnoreCase))
{
FontStretch fs = FontStretches.Normal;
try
{
fs = (FontStretch)new FontStretchConverter().ConvertFromString(val);
}
catch (Exception)
{
fs = FontStretches.Normal;
}
part.mInline.FontStretch = fs;
}
else if (key.Equals("FontStyle", StringComparison.InvariantCultureIgnoreCase))
{
FontStyle fs = FontStyles.Normal;
try
{
fs = (FontStyle)new FontStyleConverter().ConvertFromString(val);
}
catch (Exception)
{
fs = FontStyles.Normal;
}
part.mInline.FontStyle = fs;
}
else if (key.Equals("FontFamily", StringComparison.InvariantCultureIgnoreCase))
{
if (!String.IsNullOrEmpty(val))
{
FontFamily ff = new FontFamily(val);
if (Fonts.SystemFontFamilies.Contains(ff))
{
part.mInline.FontFamily = ff;
}
}
}
else if (key.Equals("Background", StringComparison.InvariantCultureIgnoreCase))
{
Brush b = part.mInline.Background;
try
{
b = (Brush)new BrushConverter().ConvertFromString(val);
}
catch (Exception)
{
b = part.mInline.Background;
}
part.mInline.Background = b;
}
else if (key.Equals("Foreground", StringComparison.InvariantCultureIgnoreCase))
{
Brush b = part.mInline.Foreground;
try
{
b = (Brush)new BrushConverter().ConvertFromString(val);
}
catch (Exception)
{
b = part.mInline.Foreground;
}
part.mInline.Foreground = b;
}
else if (key.Equals("ToolTip", StringComparison.InvariantCultureIgnoreCase))
{
part.mInline.ToolTip = val;
}
else if (key.Equals("Text", StringComparison.InvariantCultureIgnoreCase) && part.mInline is Run)
{
(part.mInline as Run).Text = val;
}
else if (key.Equals("NavigateUri", StringComparison.InvariantCultureIgnoreCase) && part.mInline is Hyperlink)
{
(part.mInline as Hyperlink).NavigateUri = new Uri(val);
}
m2 = m2.NextMatch();
}
}
if (part.mInline != null)
{
if (tag.TrimEnd().EndsWith("/"))
{
if (prevPart.mChildren != null)
{
prevPart.mChildren.Add(part.mInline);
}
}
else
{
parts.Add(part);
}
}
}
lastIdx = m.Index + m.Length;
m = m.NextMatch();
}
if (lastIdx < (len - 1))
{
root.mChildren.Add(new Run(s.Substring(lastIdx)));
}
}
}
}
The same I have implemented using Behavior. Code given below:
public class FormatTextBlock : Behavior<System.Windows.Controls.TextBlock>
{
public static readonly DependencyProperty FormattedTextProperty =
DependencyProperty.Register(
"FormattedText",
typeof(string),
typeof(FormatTextBlock),
new PropertyMetadata(string.Empty, OnFormattedTextChanged));
public string FormattedText
{
get { return (string)AssociatedObject.GetValue(FormattedTextProperty); }
set { AssociatedObject.SetValue(FormattedTextProperty, value); }
}
private static void OnFormattedTextChanged(DependencyObject textBlock, DependencyPropertyChangedEventArgs eventArgs)
{
System.Windows.Controls.TextBlock currentTxtBlock = (textBlock as FormatTextBlock).AssociatedObject;
string text = eventArgs.NewValue as string;
if (currentTxtBlock != null)
{
currentTxtBlock.Inlines.Clear();
string[] strs = text.Split(new string[] { "<Bold>", "</Bold>" }, StringSplitOptions.None);
for (int i = 0; i < strs.Length; i++)
{
currentTxtBlock.Inlines.Add(new Run { Text = strs[i], FontWeight = i % 2 == 1 ? FontWeights.Bold : FontWeights.Normal });
}
}
}
}
XAML - import namespace
<UserControl x:Class="MyClass"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:behav="clr-namespace:myAssembly.myNameSapce;assembly=myAssembly"
>
Then to use the behavior as:
<TextBlock TextWrapping="Wrap">
<i:Interaction.Behaviors>
<behav:FormatTextBlock FormattedText="{Binding Path=UIMessage}" />
</i:Interaction.Behaviors>
</TextBlock>
This work for me:
XAML:
<phone:PhoneApplicationPage x:Class="MyAPP.Views.Class"
xmlns:utils="clr-namespace:MyAPP.Utils">
and your TextBlock XAML:
<TextBlock utils:TextBlockHelper.FormattedText="{Binding Text}" />
CODE:
public static class TextBlockHelper
{
public static string GetFormattedText(DependencyObject textBlock)
{
return (string)textBlock.GetValue(FormattedTextProperty);
}
public static void SetFormattedText(DependencyObject textBlock, string value)
{
textBlock.SetValue(FormattedTextProperty, value);
}
public static readonly DependencyProperty FormattedTextProperty =
DependencyProperty.RegisterAttached("FormattedText", typeof(string), typeof(TextBlock),
new PropertyMetadata(string.Empty, (sender, e) =>
{
string text = e.NewValue as string;
var textB1 = sender as TextBlock;
if (textB1 != null)
{
textB1.Inlines.Clear();
var str = text.Split(new string[] { "<b>", "</b>" }, StringSplitOptions.None);
for (int i = 0; i < str.Length; i++)
textB1.Inlines.Add(new Run { Text = str[i], FontWeight = i % 2 == 1 ? FontWeights.Bold : FontWeights.Normal });
}
}));
}
USE in your string binding:
String Text = Text <b>Bold</b>;
So, combining a behavior to get the attached property and Jodrells use of XamlReader, here's a version that can deal with most things you would expect to be able to have as inlines in a TextBlock. Only the default and x: namespaces supperted, but you could extend that.
public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached(
"FormattedText",
typeof(string),
typeof(TextBlockBehaviour),
new PropertyMetadata(default(string), FormattedTextChanged_));
public static bool GetFormattedText(TextBlock textBlock)
{
return (bool)textBlock.GetValue(FormattedTextProperty);
}
public static void SetFormattedText(TextBlock textBlock, bool value)
{
textBlock.SetValue(FormattedTextProperty, value);
}
private static void FormattedTextChanged_(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TextBlock textBlock = d as TextBlock;
if (textBlock == null)
return;
textBlock.Inlines.Clear();
string value = e.NewValue as string;
if (string.IsNullOrEmpty(value))
{
textBlock.Text = null;
return;
}
using (var stringReader = new StringReader($"<TextBlock xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\">{value}</TextBlock>"))
{
using (var xmlReader = XmlReader.Create(stringReader))
{
TextBlock newTextBlock = (TextBlock)XamlReader.Load(xmlReader);
if (newTextBlock.Inlines.Count == 0)
{
textBlock.Text = newTextBlock.Text;
}
else
{
foreach (var inline in newTextBlock.Inlines.ToArray())
{
textBlock.Inlines.Add(inline);
}
}
}
}
}

Silverlight MVVM Validation in a DataForm

I am using generic data classes, so I can't use ria services attributes to control my validation - so I am looking for a way to manualy set up validation to work in a DataForm.
public partial class DataValue
{
private Dictionary<string, string> _errors = new Dictionary<string, string>();
public Dictionary<string, string> Errors
{
get { return _errors; }
}
public Object Value
{
get
{
object result = new object();
switch ((DataType)this.ModelEntity.ModelItem.DataType)
{
case DataType.Money:
return result = this.ValueText.ParseNullableFloat();
case DataType.Integer:
return result = this.ValueText.ParseNullableInt();
case DataType.Date:
case DataType.Time:
return result = this.ValueText.ParseNullableDateTime();
case DataType.CheckBox:
return result = this.ValueText;
default:
return result = this.ValueText;
}
}
set
{
if (!String.IsNullOrEmpty(value.ToString()))
{
bool invalid = false;
switch ((DataType)this.ModelEntity.ModelItem.DataType)
{
case DataType.Money:
float val;
if (!float.TryParse(value.ToString(), out val)) invalid = true;
break;
case DataType.Integer:
int val2;
if (!Int32.TryParse(value.ToString(), out val2)) invalid = true;
break;
case DataType.Date:
case DataType.Time:
DateTime val3;
if (!DateTime.TryParse(value.ToString(), out val3)) invalid = true;
break;
}
if (invalid == false)
ValueText = value.ToString();
else
{
ValueText = "";
_errors.Add(this.ModelEntity.LocalName, "error writing " + value.ToString() + " to " + this.ModelEntity.ModelItem.Label);
}
}
else
ValueText = "";
}
}
public partial class ModelValidater : INotifyPropertyChanging, INotifyPropertyChanged
{
private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty);
private int _ModelValidatorId;
private int _ModelEntityId;
private int _ValidatorType;
private string _ValidatorParameters;
So in ASP MVC, I simply manually checked against these rules when the form was submitted... which I guess is pretty much what I want to do in MVVM (I am just not sure the best way to go about this).
ASP Code
protected bool ModelErrors(RecordDictionary record)
{
bool result = false;
foreach (var field in record)
{
foreach (var error in field.Value.Errors)
{
result = true;
ModelState.AddModelError(error.Key + "Validation", error.Value.ToString());
}
}
return result;
}
Silverlight 3 built-in validation is based on exceptions.
Just throw a meaningful exception in your generic Setter and you should be fine.
Remember to set ValidatesOnException=True and NotifyOnValidationError=True on your {Binding}.
Jesse has a good sample of validation with exceptions on his blog.
You can attach the validation attributes using the MetadataTypeAttribute attribute.
RIA Services will automatically generate these validation on the client for you once they're exposed in the DomainService.
Example:
[MetadataType(typeof(ContactMd))]
public partial class Contact
{
internal class ContactMd
{
[MyCustomValidation]
public string Name { get; set; }
}
}
(MyCustomValidation refers to anything that inherits from ValidationAttribute).

Resources