I've searched for and found many questions similar mine but none of them seem to be quite my situation. Admittedly, mine is something of an edge-case. I'm hoping someone can spot what I'm missing here.
I've long been using a custom ItemsControl which derives from MultiSelector. I have a custom DataTemplate to draw the items in it. And they're drawn just fine if and only if I use the ItemTemplate property on the control.
But when I try instead to the ItemTemplateSelector property, my override of SelectTemplate is not being called. I've verified that it's creates and then set as the control's ItemTemplateSelector. But the breakpoint for its SelectTemplate override never gets hit.
The net effect is that the nice shapes that were previously being drawn beautifully by my one and only DataTemplate now just show up as string names.
The view I'm using is like this:
<UserControl x:Class="MyApp.Shapes.ShapeCanvas"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:gcs="clr-namespace:MyApp.Shapes"
xmlns:gcp="clr-namespace:MyApp.Properties"
xmlns:net="http://schemas.mycompany.com/mobile/net"
>
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/MyApp.Core;component/Resources/Styles/ShapeItemStyle.xaml" />
</ResourceDictionary.MergedDictionaries>
<!--
Default data template for most ShapeVm types, custom data type for PolyLineVm
I've removed the contents for brevity but they draw Paths objects normally
-->
<DataTemplate x:Key="ShapeTemplate" DataType="{x:Type gcs:ShapeVm}"/>
<DataTemplate x:Key="PolylineTemplate" DataType="{x:Type gcs:PolyLineVm}"/>
<!-- ShapeTemplateSelector to pick the right one -->
<gcs:ShapeTemplateSelector x:Key="ShapeTemplateSelector"
DefaultTemplate="{StaticResource ShapeTemplate}"
PolyLineTemplate="{StaticResource PolylineTemplate}"/>
</ResourceDictionary>
</UserControl.Resources>
<Canvas x:Name="Scene">
<gcs:ShapesControl x:Name="ShapesControl"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ItemContainerStyle="{StaticResource ShapeItemStyle}"
ItemsSource="{Binding Shapes}"
ItemTemplateSelector="{StaticResource ShapeTemplateSelector}"
>
<!-- If I use this line instead of ItemContainerStyle, It *does* pick up shape template -->
<!-- ItemTemplate="{StaticResource ShapeTemplate}" -->
<gcs:ShapesControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="Transparent" IsItemsHost="True" />
</ItemsPanelTemplate>
</gcs:ShapesControl.ItemsPanel>
</gcs:ShapesControl>
</Canvas>
</UserControl>
Custom DataTemplateSelector
public class ShapeTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
*** THIS NEVER EVEN GETS CALLED ***
return item is PolyLineVm ? PolyLineTemplate : DefaultTemplate;
}
public DataTemplate PolyLineTemplate { get; set; }
public DataTemplate DefaultTemplate { get; set; }
}
Custom MultiSelector ("ShapesControl")
using System.Collections.Specialized;
using System.Windows.Controls;
namespace MyApp.Shapes
{
// Created By:
// Date: 2017-08-25
using System.Linq;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
/// <summary>
/// ShapesControl - our own version of a WPF MultiSelector. Basically an
/// ItemsControl that can select multiple items at once. We need this to
/// handle user manipulation of shapes on the ShapeCanvas and, potentially,
/// elsewhere.
/// </summary>
public class ShapesControl : MultiSelector
{
protected override bool IsItemItsOwnContainerOverride(object item)
{
return (item is ShapeItem);
}
protected override DependencyObject GetContainerForItemOverride()
{
// Each item we display is wrapped in our own container: ShapeItem
// This override is how we enable that.
return new ShapeItem();
}
// ...Other handlers are multi-selection overrides and removed for clarity
}
}
Finally, the ShapeItemStyle I use to draw my custom ShapeItems
<Style x:Key="ShapeItemStyle"
TargetType="{x:Type gcs:ShapeItem}"
d:DataContext="{d:DesignInstance {x:Type gcs:ShapeVm}}"
>
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Canvas.Left" Value="{Binding Path=Left, Mode=OneWay}"/>
<Setter Property="Canvas.Top" Value="{Binding Path=Top, Mode=OneWay}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type gcs:ShapeItem}">
<Grid>
>
<!-- ContentPresenter for the ShapeVm that this ShapeItem contains -->
<ContentPresenter x:Name="PART_Shape"
Content="{TemplateBinding ContentControl.Content}"
ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}"
ContentStringFormat="{TemplateBinding ContentControl.ContentStringFormat}"
HorizontalAlignment="{TemplateBinding Control.HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding Control.VerticalContentAlignment}"
IsHitTestVisible="False"
SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}"
RenderTransformOrigin="{TemplateBinding ContentControl.RenderTransformOrigin}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
(Edit to add ShapeItem per request. Note that this includes selection code that interacts with the custom MultiSelector ("ShapesControl") above. I removed some of those functions from ShapesControl code for brevity as they're triggered by mouse-clicks and I couldn't see how they could possibly prevent a custom DataTemplateSelector from being invoked. But I've posted all of ShapeItem here)
namespace MyApp.Shapes
{
[TemplatePart(Name="PART_Shape", Type=typeof(ContentPresenter))]
public class ShapeItem : ContentControl
{
public ShapeVm ShapeVm => DataContext as ShapeVm;
public ShapeType ShapeType => ShapeVm?.ShapeType ?? ShapeType.None;
static ShapeItem()
{
DefaultStyleKeyProperty.OverrideMetadata
(typeof(ShapeItem),
new FrameworkPropertyMetadata(typeof(ShapeItem)));
}
private bool WasSelectedWhenManipulationStarted { get; set; }
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
ParentSelector?.NotifyItemClicked(this, true);
e.Handled = true;
}
// The following ShapeItem manipulation handlers only handle the case of
// moving the shape as a whole across the canvas. These handlers do NOT
// handle the case of resizing the shape. Those handlers are on the
// ShapeHandleThumb class.
protected override void OnManipulationStarting(ManipulationStartingEventArgs e)
{
// The Canvas defines the coordinates for manipulation
e.ManipulationContainer = this.FindVisualParent<Canvas>();
base.OnManipulationStarting(e);
e.Handled = true;
}
protected override void OnManipulationStarted(ManipulationStartedEventArgs e)
{
Debug.Assert(e.ManipulationContainer is Canvas);
base.OnManipulationStarted(e);
// If this item was selected already, this manipulation might be a
// move. In that case, wait until we're done with the manipulation
// before deciding whether to notify the control.
if (IsSelected)
WasSelectedWhenManipulationStarted = true;
else
ParentSelector.NotifyItemClicked(this, false);
e.Handled = true;
}
protected override void OnManipulationDelta(ManipulationDeltaEventArgs e)
{
Debug.Assert(e.ManipulationContainer is Canvas);
base.OnManipulationDelta(e);
ParentSelector.NotifyItemMoved(e.DeltaManipulation.Translation);
e.Handled = true;
}
protected override void OnManipulationCompleted(ManipulationCompletedEventArgs e)
{
Debug.Assert(e.ManipulationContainer is Canvas);
base.OnManipulationCompleted(e);
if (WasSelectedWhenManipulationStarted)
{
// If nothing moved appreciably, unselect everything. This is how I
// am detecting just a Tap. I have to think there is a better way...
var t = e.TotalManipulation.Translation;
if (Math.Abs(t.X) < 0.0001 && Math.Abs(t.Y) < 0.0001)
ParentSelector.NotifyItemClicked(this, false);
}
e.Handled = true;
}
private bool IsControlKeyPressed =>
(Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control;
internal ShapesControl ParentSelector =>
ItemsControl.ItemsControlFromItemContainer(this) as ShapesControl;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
Debug.Assert(ReferenceEquals(
ParentSelector.ItemContainerGenerator.ItemFromContainer(this),
ShapeVm));
}
public static readonly DependencyProperty IsSelectedProperty =
Selector.IsSelectedProperty.AddOwner(
typeof(ShapeItem),
new FrameworkPropertyMetadata(false, OnIsSelectedChanged));
public bool IsSelected
{
get
{
var value = GetValue(IsSelectedProperty);
return value != null && (bool)value;
}
set { SetValue(IsSelectedProperty, value); }
}
private static void OnIsSelectedChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
if (!(target is ShapeItem item))
return;
var evt = (bool)e.NewValue ? Selector.SelectedEvent : Selector.UnselectedEvent;
item.RaiseEvent(new RoutedEventArgs(evt, item));
}
}
}
The problem is with following code:
protected override DependencyObject GetContainerForItemOverride()
{
// Each item we display is wrapped in our own container: ShapeItem
// This override is how we enable that.
return new ShapeItem();
}
When you override GetContainerForItemOverride method, it is your code's responsibility to use ItemTemplateSelector and attach it to ItemsControl.
I have a Button containing a Hyperlink, like so:
<Button IsEnabled="False">
<Hyperlink IsEnabled="True">Testing</Hyperlink>
</Button>
I need the Hyperlink to be enabled, however the Button to be disabled. How can I achieve this?
The above simply results in both controls being disabled.
I solved this problem by creating a simple wrapper element that breaks the IsEnabled inheritance chain from the parent.
The framework's default coerce callback checks the parent IsEnabled value and inherits it. This control sets a new coerce callback that just returns the value directly without checking inheritance.
public class ResetIsEnabled : ContentControl
{
static ResetIsEnabled()
{
IsEnabledProperty.OverrideMetadata(
typeof(ResetIsEnabled),
new UIPropertyMetadata(
defaultValue: true,
propertyChangedCallback: (_, __) => { },
coerceValueCallback: (_, x) => x));
}
}
In the example from the question it would be used like this:
<Button IsEnabled="False">
<ResetIsEnabled>
<!-- Child elements within ResetIsEnabled have IsEnabled set to true (the default value) -->
<Hyperlink>Testing</Hyperlink>
</ResetIsEnabled>
</Button>
Control Hyperlink has strangely with the property IsEnabled. In addition to the one that you mentioned, namely, the full value inheritance from a parent, there is another similar.
Hyperlink for the specific control, which has been turned off (IsEnabled="False"), setting (IsEnabled="True") will not update the Hyperlink property. The solution - use a relative source for Hyperlink (more info).
For solving your question, I have decided that it is not the standard way to solve. So I created a Class with its own dependencies properties. It has it's property MyIsEnabled and MyStyle. As you might guess from the title, the first sets its property IsEnabled and MyStyle need to specify the button style, simulating the IsEnabled="False" behavior.
SimulateDisable Style
<Style x:Key="SimulateDisable" TargetType="{x:Type Button}">
<Setter Property="Opacity" Value="0.5" />
<Setter Property="Background" Value="Gainsboro" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border CornerRadius="4" BorderThickness="1" BorderBrush="DarkBlue" SnapsToDevicePixels="True">
<ContentPresenter x:Name="MyContentPresenter" Content="{TemplateBinding Content}" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Define Button with yours properties:
<Button Name="MyButton" local:MyClass.MyIsEnabled="False" local:MyClass.MyStyle="{StaticResource SimulateDisable}" Width="100" Height="30" Click="Button_Click">
<Hyperlink IsEnabled="True" Click="Hyperlink_Click">Testing</Hyperlink>
</Button>
Listing of MyClass
public class MyClass : DependencyObject
{
public static readonly DependencyProperty MyIsEnabledProperty;
public static readonly DependencyProperty MyStyleProperty;
#region MyIsEnabled
public static void SetMyIsEnabled(DependencyObject DepObject, bool value)
{
DepObject.SetValue(MyIsEnabledProperty, value);
}
public static bool GetMyIsEnabled(DependencyObject DepObject)
{
return (bool)DepObject.GetValue(MyIsEnabledProperty);
}
#endregion MyIsEnabled
#region MyStyle
public static void SetMyStyle(DependencyObject DepObject, Style value)
{
DepObject.SetValue(MyStyleProperty, value);
}
public static Style GetMyStyle(DependencyObject DepObject)
{
return (Style)DepObject.GetValue(MyStyleProperty);
}
#endregion MyStyle
static MyClass()
{
MyIsEnabledProperty = DependencyProperty.RegisterAttached("MyIsEnabled",
typeof(bool),
typeof(MyClass),
new UIPropertyMetadata(false, OnPropertyChanged));
MyStyleProperty = DependencyProperty.RegisterAttached("MyStyle",
typeof(Style),
typeof(MyClass),
new UIPropertyMetadata(OnPropertyChanged));
}
private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
Button MyButton = sender as Button;
bool MyBool = GetMyIsEnabled(MyButton);
if (MyBool == false)
{
MyButton.Style = MyClass.GetMyStyle(MyButton);
}
}
}
Plus for the event Hyperlink pointing e.Handled = true, so that the event did not happen next.
private void Hyperlink_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("Hyperlink Click!");
e.Handled = true;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("Button Click! Don't show it's!");
}
Output
P.S. Sorry for late answer :).
In WPF, is it possible to bind the key in "{StaticResource key}"to a variable.
For example. I have a variable ExecutionState with the states Active and Completed.
In my ResourceDictionary I have
<Style TargetType="{x:Type TextBlock}" x:Key="Active">
<Setter Property="Foreground" Value="Yellow"/>
</Style>
<Style TargetType="{x:Type TextBlock}" x:Key="Completed">
<Setter Property="Foreground" Value="Green"/>
</Style>
Instead of having
<TextBlock Style="{StaticResource Active}"/>
I Would like to have something like
<TextBlock Style="{StaticResource {Binding ExecutionState}}"/>
Thus if the state changes the text color changes.
Is something like this even possible?
I can achieve the wanted functionality using triggers, but I have to reuse it at several places and I don't want to clutter my code.
I am using MVVM also.
thanx
No, it's not possible. Binding can only be set on a DependencyProperty. StaticResource is not a DependencyObject, so there is no DependencyProperty. You should use Trigger(s) or develop your own attached behavior.
There is no direct way to achieve .
Create one attached property and assign the property name to bind.
In the property change callback function update control style.
<TextBlock dep:CustomStyle.StyleName="{Binding ExecutionState}" Text="Thiru" />
public static class CustomStyle
{
static FrameworkPropertyMetadata _styleMetadata = new FrameworkPropertyMetadata(
string.Empty, FrameworkPropertyMetadataOptions.AffectsRender, StyleNamePropertyChangeCallBack);
public static readonly DependencyProperty StyleNameProperty =
DependencyProperty.RegisterAttached("StyleName", typeof (String), typeof (CustomStyle), _styleMetadata);
public static void SetStyleName(UIElement element, string value)
{
element.SetValue(StyleNameProperty, value);
}
public static Boolean GetStyleName(UIElement element)
{
return (Boolean)element.GetValue(StyleNameProperty);
}
public static void StyleNamePropertyChangeCallBack(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
FrameworkElement ctrl = d as FrameworkElement;
if (ctrl.IsLoaded)
{
string styleName = Convert.ToString(e.NewValue);
if (!string.IsNullOrEmpty(styleName))
{
ctrl.Style = ctrl.TryFindResource(styleName) as Style;
}
}
}
}
I can trigger property settings on my ListBoxItem template based on properties of underlying data object using DataTrigger with something like this
<DataTrigger Binding="{Binding Path=IsMouseOver}" Value="true">
<Setter TargetName="ItemText" Property="TextBlock.TextDecorations" Value="Underline">
</Setter>
</DataTrigger>
But what if I want to do the opposite? I mean set a property value on the underlying data object based on property value of my ListBoxItem. Something like:
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="MyClass.IsHilited" Value="True"></Setter>
</Trigger>
Is there a mechanism for something like this or what would be the recommended approach to deal with situations like this?
Thanks.
I think that you could use an EventSetter to do in XAML what Josh G suggested in code. Maybe create one for the MouseEnter and MouseLeave events, and style the control appropriately for each?
Update: You can set up the events like this:
<ListBox>
<ListBox.Resources>
<Style TargetType="{x:Type ListBoxItem}">
<EventSetter Event="MouseEnter" Handler="OnListBoxItemMouseEnter" />
<EventSetter Event="MouseLeave" Handler="OnListBoxItemMouseLeave" />
</Style>
</ListBox.Resources>
<ListBoxItem>Item 1</ListBoxItem>
<ListBoxItem>Item 2</ListBoxItem>
<ListBoxItem>Item 3</ListBoxItem>
<ListBoxItem>Item 4</ListBoxItem>
<ListBoxItem>Item 5</ListBoxItem>
</ListBox>
The Style registers for the MouseEnter and MouseLeave events for all ListBoxItems defined in that ListBox.
And then in your code behind file:
private void OnListBoxItemMouseEnter(object sender, RoutedEventArgs e)
{
ListBoxItem lvi = sender as ListBoxItem;
if(null != lvi)
{
lvi.Foreground = new SolidColorBrush(Colors.Red);
}
}
private void OnListBoxItemMouseLeave(object sender, RoutedEventArgs e)
{
ListBoxItem lvi = sender as ListBoxItem;
if(null != lvi)
{
lvi.Foreground = new SolidColorBrush(Colors.Black);
}
}
These handlers set the color of the text of the ListBoxItem to red when the mouse is over the item, and back to black when the mouse leaves it.
WPF triggers are intended for causing visual changes. Setter objects within triggers cause property changes on the control.
If you want to respond to an event (like an EventTrigger), you could always simply subscribe to the event in code and then set the data property in the handler.
You could use MouseEnter and MouseLeave in this way. For example:
listBox.MouseEnter += listBox_MouseEnter;
listBox.MouseLeave += listBox_MouseLeave;
void listBox_MouseEnter(object sender, MouseEventArgs e)
{
listBox.MyClass.IsHilited = true;
}
void listBox_MouseLeave(object sender, MouseEventArgs e)
{
listBox.MyClass.IsHilited = false;
}
Some properties on a control you could bind the property of the data object to, like so:
Binding myBind = new Binding("IsHilited");
myBind.Source = listBox.DataContext;
listBox.SetBinding(listBox.IsEnabled, myBind);
You can't use IsMouseOver in a binding, however.
If you create a custom control you can have even greater flexibility to build a binding like this into the control. You could create a custom depency property and sync it with the data property in the DependencyPropertyChanged handler. You could then set this dependency property with a WPF trigger.
Here's an example:
public static readonly DependencyProperty IsHilitedProperty =
DependencyProperty.Register("IsHilited", typeof(bool), typeof(CustomListBox),
new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnIsHilitedChanged)));
public double IsHilited
{
get
{
return (bool)GetValue(IsHilitedProperty);
}
set
{
SetValue(IsHilitedProperty, value);
}
}
private static void OnIsHilitedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
CustomListBox box = obj as CustomListBox;
if (box != null)
box.MyClass.IsHilited = box.IsHilited;
// Or:
// Class myClass = box.DataContext as Class;
// myClass.IsHilited = box.isHilited;
}
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="IsHilited" Value="True"/>
</Trigger>
How to allow TextBlock's text to be selectable?
I tried to get it to work by displaying the text using a read-only TextBox styled to look like a textblock but this will not work in my case because a TextBox does not have inlines. In other words, how to make it selectable?
Use a TextBox with these settings instead to make it read only and to look like a TextBlock control.
<TextBox Background="Transparent"
BorderThickness="0"
Text="{Binding Text, Mode=OneWay}"
IsReadOnly="True"
TextWrapping="Wrap" />
All the answers here are just using a TextBox or trying to implement text selection manually, which leads to poor performance or non-native behaviour (blinking caret in TextBox, no keyboard support in manual implementations etc.)
After hours of digging around and reading the WPF source code, I instead discovered a way of enabling the native WPF text selection for TextBlock controls (or really any other controls). Most of the functionality around text selection is implemented in System.Windows.Documents.TextEditor system class.
To enable text selection for your control you need to do two things:
Call TextEditor.RegisterCommandHandlers() once to register class
event handlers
Create an instance of TextEditor for each instance of your class and pass the underlying instance of your System.Windows.Documents.ITextContainer to it
There's also a requirement that your control's Focusable property is set to True.
This is it! Sounds easy, but unfortunately TextEditor class is marked as internal. So I had to write a reflection wrapper around it:
class TextEditorWrapper
{
private static readonly Type TextEditorType = Type.GetType("System.Windows.Documents.TextEditor, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
private static readonly PropertyInfo IsReadOnlyProp = TextEditorType.GetProperty("IsReadOnly", BindingFlags.Instance | BindingFlags.NonPublic);
private static readonly PropertyInfo TextViewProp = TextEditorType.GetProperty("TextView", BindingFlags.Instance | BindingFlags.NonPublic);
private static readonly MethodInfo RegisterMethod = TextEditorType.GetMethod("RegisterCommandHandlers",
BindingFlags.Static | BindingFlags.NonPublic, null, new[] { typeof(Type), typeof(bool), typeof(bool), typeof(bool) }, null);
private static readonly Type TextContainerType = Type.GetType("System.Windows.Documents.ITextContainer, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
private static readonly PropertyInfo TextContainerTextViewProp = TextContainerType.GetProperty("TextView");
private static readonly PropertyInfo TextContainerProp = typeof(TextBlock).GetProperty("TextContainer", BindingFlags.Instance | BindingFlags.NonPublic);
public static void RegisterCommandHandlers(Type controlType, bool acceptsRichContent, bool readOnly, bool registerEventListeners)
{
RegisterMethod.Invoke(null, new object[] { controlType, acceptsRichContent, readOnly, registerEventListeners });
}
public static TextEditorWrapper CreateFor(TextBlock tb)
{
var textContainer = TextContainerProp.GetValue(tb);
var editor = new TextEditorWrapper(textContainer, tb, false);
IsReadOnlyProp.SetValue(editor._editor, true);
TextViewProp.SetValue(editor._editor, TextContainerTextViewProp.GetValue(textContainer));
return editor;
}
private readonly object _editor;
public TextEditorWrapper(object textContainer, FrameworkElement uiScope, bool isUndoEnabled)
{
_editor = Activator.CreateInstance(TextEditorType, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.CreateInstance,
null, new[] { textContainer, uiScope, isUndoEnabled }, null);
}
}
I also created a SelectableTextBlock derived from TextBlock that takes the steps noted above:
public class SelectableTextBlock : TextBlock
{
static SelectableTextBlock()
{
FocusableProperty.OverrideMetadata(typeof(SelectableTextBlock), new FrameworkPropertyMetadata(true));
TextEditorWrapper.RegisterCommandHandlers(typeof(SelectableTextBlock), true, true, true);
// remove the focus rectangle around the control
FocusVisualStyleProperty.OverrideMetadata(typeof(SelectableTextBlock), new FrameworkPropertyMetadata((object)null));
}
private readonly TextEditorWrapper _editor;
public SelectableTextBlock()
{
_editor = TextEditorWrapper.CreateFor(this);
}
}
Another option would be to create an attached property for TextBlock to enable text selection on demand. In this case, to disable the selection again, one needs to detach a TextEditor by using the reflection equivalent of this code:
_editor.TextContainer.TextView = null;
_editor.OnDetach();
_editor = null;
I have been unable to find any example of really answering the question. All the answers used a Textbox or RichTextbox. I needed a solution that allowed me to use a TextBlock, and this is the solution I created.
I believe the correct way to do this is to extend the TextBlock class. This is the code I used to extend the TextBlock class to allow me to select the text and copy it to clipboard. "sdo" is the namespace reference I used in the WPF.
WPF Using Extended Class:
xmlns:sdo="clr-namespace:iFaceCaseMain"
<sdo:TextBlockMoo x:Name="txtResults" Background="Black" Margin="5,5,5,5"
Foreground="GreenYellow" FontSize="14" FontFamily="Courier New"></TextBlockMoo>
Code Behind for Extended Class:
public partial class TextBlockMoo : TextBlock
{
TextPointer StartSelectPosition;
TextPointer EndSelectPosition;
public String SelectedText = "";
public delegate void TextSelectedHandler(string SelectedText);
public event TextSelectedHandler TextSelected;
protected override void OnMouseDown(MouseButtonEventArgs e)
{
base.OnMouseDown(e);
Point mouseDownPoint = e.GetPosition(this);
StartSelectPosition = this.GetPositionFromPoint(mouseDownPoint, true);
}
protected override void OnMouseUp(MouseButtonEventArgs e)
{
base.OnMouseUp(e);
Point mouseUpPoint = e.GetPosition(this);
EndSelectPosition = this.GetPositionFromPoint(mouseUpPoint, true);
TextRange otr = new TextRange(this.ContentStart, this.ContentEnd);
otr.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush(Colors.GreenYellow));
TextRange ntr = new TextRange(StartSelectPosition, EndSelectPosition);
ntr.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush(Colors.White));
SelectedText = ntr.Text;
if (!(TextSelected == null))
{
TextSelected(SelectedText);
}
}
}
Example Window Code:
public ucExample(IInstanceHost host, ref String WindowTitle, String ApplicationID, String Parameters)
{
InitializeComponent();
/*Used to add selected text to clipboard*/
this.txtResults.TextSelected += txtResults_TextSelected;
}
void txtResults_TextSelected(string SelectedText)
{
Clipboard.SetText(SelectedText);
}
Create ControlTemplate for the TextBlock and put a TextBox inside with readonly property set.
Or just use TextBox and make it readonly, then you can change the TextBox.Style to make it looks like TextBlock.
Apply this style to your TextBox and that's it (inspired from this article):
<Style x:Key="SelectableTextBlockLikeStyle" TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="IsReadOnly" Value="True"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="-2,0,0,0"/>
<!-- The Padding -2,0,0,0 is required because the TextBox
seems to have an inherent "Padding" of about 2 pixels.
Without the Padding property,
the text seems to be 2 pixels to the left
compared to a TextBlock
-->
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="False" />
<Condition Property="IsFocused" Value="False" />
</MultiTrigger.Conditions>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<TextBlock Text="{TemplateBinding Text}"
FontSize="{TemplateBinding FontSize}"
FontStyle="{TemplateBinding FontStyle}"
FontFamily="{TemplateBinding FontFamily}"
FontWeight="{TemplateBinding FontWeight}"
TextWrapping="{TemplateBinding TextWrapping}"
Foreground="{DynamicResource NormalText}"
Padding="0,0,0,0"
/>
</ControlTemplate>
</Setter.Value>
</Setter>
</MultiTrigger>
</Style.Triggers>
</Style>
I'm not sure if you can make a TextBlock selectable, but another option would be to use a RichTextBox - it is like a TextBox as you suggested, but supports the formatting you want.
According to Windows Dev Center:
TextBlock.IsTextSelectionEnabled property
[ Updated for UWP apps on Windows 10. For Windows 8.x articles, see
the archive ]
Gets or sets a value that indicates whether text selection is enabled
in the TextBlock, either through user action or calling
selection-related API.
While the question does say 'Selectable' I believe the intentional results is to get the text to the clipboard. This can easily and elegantly be achieved by adding a Context Menu and menu item called copy that puts the Textblock Text property value in clipboard. Just an idea anyway.
TextBlock does not have a template. So inorder to achieve this, we need to use a TextBox whose style is changed to behave as a textBlock.
<Style x:Key="TextBlockUsingTextBoxStyle" BasedOn="{x:Null}" TargetType="{x:Type TextBox}">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource TextBoxBorder}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="1"/>
<Setter Property="AllowDrop" Value="true"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/>
<Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<TextBox BorderThickness="{TemplateBinding BorderThickness}" IsReadOnly="True" Text="{TemplateBinding Text}" Background="{x:Null}" BorderBrush="{x:Null}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
There is an alternative solution that might be adaptable to the RichTextBox oultined in this blog post - it used a trigger to swap out the control template when the use hovers over the control - should help with performance
new TextBox
{
Text = text,
TextAlignment = TextAlignment.Center,
TextWrapping = TextWrapping.Wrap,
IsReadOnly = true,
Background = Brushes.Transparent,
BorderThickness = new Thickness()
{
Top = 0,
Bottom = 0,
Left = 0,
Right = 0
}
};
Here is what worked for me. I created a class TextBlockEx that is derived from TextBox and is set read-only, and text wrap in the constructor.
public class TextBlockEx : TextBox
{
public TextBlockEx()
{
base.BorderThickness = new Thickness(0);
IsReadOnly = true;
TextWrapping = TextWrapping.Wrap;
//Background = Brushes.Transparent; // Uncomment to get parent's background color
}
}
Really nice and easy solution, exactly what I wanted !
I bring some small modifications
public class TextBlockMoo : TextBlock
{
public String SelectedText = "";
public delegate void TextSelectedHandler(string SelectedText);
public event TextSelectedHandler OnTextSelected;
protected void RaiseEvent()
{
if (OnTextSelected != null){OnTextSelected(SelectedText);}
}
TextPointer StartSelectPosition;
TextPointer EndSelectPosition;
Brush _saveForeGroundBrush;
Brush _saveBackGroundBrush;
TextRange _ntr = null;
protected override void OnMouseDown(MouseButtonEventArgs e)
{
base.OnMouseDown(e);
if (_ntr!=null) {
_ntr.ApplyPropertyValue(TextElement.ForegroundProperty, _saveForeGroundBrush);
_ntr.ApplyPropertyValue(TextElement.BackgroundProperty, _saveBackGroundBrush);
}
Point mouseDownPoint = e.GetPosition(this);
StartSelectPosition = this.GetPositionFromPoint(mouseDownPoint, true);
}
protected override void OnMouseUp(MouseButtonEventArgs e)
{
base.OnMouseUp(e);
Point mouseUpPoint = e.GetPosition(this);
EndSelectPosition = this.GetPositionFromPoint(mouseUpPoint, true);
_ntr = new TextRange(StartSelectPosition, EndSelectPosition);
// keep saved
_saveForeGroundBrush = (Brush)_ntr.GetPropertyValue(TextElement.ForegroundProperty);
_saveBackGroundBrush = (Brush)_ntr.GetPropertyValue(TextElement.BackgroundProperty);
// change style
_ntr.ApplyPropertyValue(TextElement.BackgroundProperty, new SolidColorBrush(Colors.Yellow));
_ntr.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush(Colors.DarkBlue));
SelectedText = _ntr.Text;
}
}
public MainPage()
{
this.InitializeComponent();
...
...
...
//Make Start result text copiable
TextBlockStatusStart.IsTextSelectionEnabled = true;
}
Adding to #torvin's answer and as #Dave Huang mentioned in the comments if you have TextTrimming="CharacterEllipsis" enabled the application crashes when you hover over the ellipsis.
I tried other options mentioned in the thread about using a TextBox but it really doesn't seem to be the solution either as it doesn't show the 'ellipsis' and also if the text is too long to fit the container selecting the content of the textbox 'scrolls' internally which isn't a TextBlock behaviour.
I think the best solution is #torvin's answer but has the nasty crash when hovering over the ellipsis.
I know it isn't pretty, but subscribing/unsubscribing internally to unhandled exceptions and handling the exception was the only way I found of solving this problem, please share if somebody has a better solution :)
public class SelectableTextBlock : TextBlock
{
static SelectableTextBlock()
{
FocusableProperty.OverrideMetadata(typeof(SelectableTextBlock), new FrameworkPropertyMetadata(true));
TextEditorWrapper.RegisterCommandHandlers(typeof(SelectableTextBlock), true, true, true);
// remove the focus rectangle around the control
FocusVisualStyleProperty.OverrideMetadata(typeof(SelectableTextBlock), new FrameworkPropertyMetadata((object)null));
}
private readonly TextEditorWrapper _editor;
public SelectableTextBlock()
{
_editor = TextEditorWrapper.CreateFor(this);
this.Loaded += (sender, args) => {
this.Dispatcher.UnhandledException -= Dispatcher_UnhandledException;
this.Dispatcher.UnhandledException += Dispatcher_UnhandledException;
};
this.Unloaded += (sender, args) => {
this.Dispatcher.UnhandledException -= Dispatcher_UnhandledException;
};
}
private void Dispatcher_UnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
if (!string.IsNullOrEmpty(e?.Exception?.StackTrace))
{
if (e.Exception.StackTrace.Contains("System.Windows.Controls.TextBlock.GetTextPositionFromDistance"))
{
e.Handled = true;
}
}
}
}
Just use a FlowDocument inside a FlowDocumentScrollViewer, passing your inlines to the element.
You can control the style of the element, in my case I added a small border.
<FlowDocumentScrollViewer Grid.Row="2" Margin="5,3" BorderThickness="1"
BorderBrush="{DynamicResource Element.Border}"
VerticalScrollBarVisibility="Auto">
<FlowDocument>
<Paragraph>
<Bold>Some bold text in the paragraph.</Bold>
Some text that is not bold.
</Paragraph>
<List>
<ListItem>
<Paragraph>ListItem 1</Paragraph>
</ListItem>
<ListItem>
<Paragraph>ListItem 2</Paragraph>
</ListItem>
<ListItem>
<Paragraph>ListItem 3</Paragraph>
</ListItem>
</List>
</FlowDocument>
</FlowDocumentScrollViewer>
I agree most answers here do not create a selectable TextBlock. #Billy Willoughby's worked well however it didn't have a visible cue for selection. I'd like to extend his extension which can highlight text as it is selected. It also incorporates double and triple click selection. You can add a context menu with a "Copy" if needed.
It uses the Background property to "highlight" the selection so it is limited in that it will overwrite Run.Background
https://github.com/mwagnerEE/WagnerControls
Added Selection & SelectionChanged Event to torvin's code
public class SelectableTextBlock : TextBlock
{
static readonly Type TextEditorType
= Type.GetType("System.Windows.Documents.TextEditor, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
static readonly PropertyInfo IsReadOnlyProp
= TextEditorType.GetProperty("IsReadOnly", BindingFlags.Instance | BindingFlags.NonPublic);
static readonly PropertyInfo TextViewProp
= TextEditorType.GetProperty("TextView", BindingFlags.Instance | BindingFlags.NonPublic);
static readonly MethodInfo RegisterMethod
= TextEditorType.GetMethod("RegisterCommandHandlers",
BindingFlags.Static | BindingFlags.NonPublic, null, new[] { typeof(Type), typeof(bool), typeof(bool), typeof(bool) }, null);
static readonly Type TextContainerType
= Type.GetType("System.Windows.Documents.ITextContainer, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
static readonly PropertyInfo TextContainerTextViewProp
= TextContainerType.GetProperty("TextView");
static readonly PropertyInfo TextContainerTextSelectionProp
= TextContainerType.GetProperty("TextSelection");
static readonly PropertyInfo TextContainerProp = typeof(TextBlock).GetProperty("TextContainer", BindingFlags.Instance | BindingFlags.NonPublic);
static void RegisterCommandHandlers(Type controlType, bool acceptsRichContent, bool readOnly, bool registerEventListeners)
{
RegisterMethod.Invoke(null, new object[] { controlType, acceptsRichContent, readOnly, registerEventListeners });
}
static SelectableTextBlock()
{
FocusableProperty.OverrideMetadata(typeof(SelectableTextBlock), new FrameworkPropertyMetadata(true));
RegisterCommandHandlers(typeof(SelectableTextBlock), true, true, true);
// remove the focus rectangle around the control
FocusVisualStyleProperty.OverrideMetadata(typeof(SelectableTextBlock), new FrameworkPropertyMetadata((object)null));
}
//private readonly TextEditorWrapper _editor;
object? textContainer;
object? editor;
public TextSelection TextSelection { get; private set; }
public SelectableTextBlock()
{
textContainer = TextContainerProp.GetValue(this);
editor = Activator.CreateInstance(TextEditorType, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.CreateInstance,
null, new[] { textContainer, this, false }, null);
IsReadOnlyProp.SetValue(editor, true);
TextViewProp.SetValue(editor, TextContainerTextViewProp.GetValue(textContainer));
TextSelection = (TextSelection)TextContainerTextSelectionProp.GetValue(textContainer);
TextSelection.Changed += (s, e) => OnSelectionChanged?.Invoke(this, e);
}
public event EventHandler OnSelectionChanged;
}
I've implemented SelectableTextBlock in my opensource controls library. You can use it like this:
<jc:SelectableTextBlock Text="Some text" />