The following textblock wraps and trims as expected. The elipsis "..." is displayed when the text is trimmed.
<TextBlock
MaxWidth="60"
MaxHeight="60"
Text="This is some long text which I would like to wrap."
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis" />
I would like to display a tooltip over the text with the full text, but only if the text is trimmed. I'm not sure how to reliably determine if the "..." is being shown or not.
How do I determine if the text is being trimmed or not?
Because the link in Alek's answer is down, I found a cached copy of the link from the wayback machine. You can not download the code linked in the article, so here is a pre-assembled version of the code. There was one or two issues I ran in to while trying to make it work so this code is slightly different then the code in the examples in the article.
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace TextBlockService
{
//Based on the project from http://web.archive.org/web/20130316081653/http://tranxcoder.wordpress.com/2008/10/12/customizing-lookful-wpf-controls-take-2/
public static class TextBlockService
{
static TextBlockService()
{
// Register for the SizeChanged event on all TextBlocks, even if the event was handled.
EventManager.RegisterClassHandler(
typeof(TextBlock),
FrameworkElement.SizeChangedEvent,
new SizeChangedEventHandler(OnTextBlockSizeChanged),
true);
}
private static readonly DependencyPropertyKey IsTextTrimmedKey = DependencyProperty.RegisterAttachedReadOnly("IsTextTrimmed",
typeof(bool),
typeof(TextBlockService),
new PropertyMetadata(false));
public static readonly DependencyProperty IsTextTrimmedProperty = IsTextTrimmedKey.DependencyProperty;
[AttachedPropertyBrowsableForType(typeof(TextBlock))]
public static Boolean GetIsTextTrimmed(TextBlock target)
{
return (Boolean)target.GetValue(IsTextTrimmedProperty);
}
public static readonly DependencyProperty AutomaticToolTipEnabledProperty = DependencyProperty.RegisterAttached(
"AutomaticToolTipEnabled",
typeof(bool),
typeof(TextBlockService),
new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.Inherits));
[AttachedPropertyBrowsableForType(typeof(DependencyObject))]
public static Boolean GetAutomaticToolTipEnabled(DependencyObject element)
{
if (null == element)
{
throw new ArgumentNullException("element");
}
return (bool)element.GetValue(AutomaticToolTipEnabledProperty);
}
public static void SetAutomaticToolTipEnabled(DependencyObject element, bool value)
{
if (null == element)
{
throw new ArgumentNullException("element");
}
element.SetValue(AutomaticToolTipEnabledProperty, value);
}
private static void OnTextBlockSizeChanged(object sender, SizeChangedEventArgs e)
{
TriggerTextRecalculation(sender);
}
private static void TriggerTextRecalculation(object sender)
{
var textBlock = sender as TextBlock;
if (null == textBlock)
{
return;
}
if (TextTrimming.None == textBlock.TextTrimming)
{
textBlock.SetValue(IsTextTrimmedKey, false);
}
else
{
//If this function is called before databinding has finished the tooltip will never show.
//This invoke defers the calculation of the text trimming till after all current pending databinding
//has completed.
var isTextTrimmed = textBlock.Dispatcher.Invoke(() => CalculateIsTextTrimmed(textBlock), DispatcherPriority.DataBind);
textBlock.SetValue(IsTextTrimmedKey, isTextTrimmed);
}
}
private static bool CalculateIsTextTrimmed(TextBlock textBlock)
{
if (!textBlock.IsArrangeValid)
{
return GetIsTextTrimmed(textBlock);
}
Typeface typeface = new Typeface(
textBlock.FontFamily,
textBlock.FontStyle,
textBlock.FontWeight,
textBlock.FontStretch);
// FormattedText is used to measure the whole width of the text held up by TextBlock container
FormattedText formattedText = new FormattedText(
textBlock.Text,
System.Threading.Thread.CurrentThread.CurrentCulture,
textBlock.FlowDirection,
typeface,
textBlock.FontSize,
textBlock.Foreground);
formattedText.MaxTextWidth = textBlock.ActualWidth;
// When the maximum text width of the FormattedText instance is set to the actual
// width of the textBlock, if the textBlock is being trimmed to fit then the formatted
// text will report a larger height than the textBlock. Should work whether the
// textBlock is single or multi-line.
// The "formattedText.MinWidth > formattedText.MaxTextWidth" check detects if any
// single line is too long to fit within the text area, this can only happen if there is a
// long span of text with no spaces.
return (formattedText.Height > textBlock.ActualHeight || formattedText.MinWidth > formattedText.MaxTextWidth);
}
}
}
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:tbs="clr-namespace:TextBlockService">
<!--
Rather than forcing *all* TextBlocks to adopt TextBlockService styles,
using x:Key allows a more friendly opt-in model.
-->
<Style TargetType="TextBlock" x:Key="TextBlockService">
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="tbs:TextBlockService.AutomaticToolTipEnabled" Value="True" />
<Condition Property="tbs:TextBlockService.IsTextTrimmed" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=Text}" />
</MultiTrigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
I haven't done a lot of WPF lately, so I'm not sure if this is what you're looking for, but check out this article: Customizing “lookful” WPF controls – Take 2. It's a bit complex, but it seems to address the same question you're asking. UPDATE: The website seems gone, but you can find the article in the archive. SEE Scott Chamberlain's ANSWER WITH THE SAMPLE CODE (thanks Scott).
The solution above didn't work for me if the TextBlock is part of a ListBoxItem DataTemplate.
I propose another solution:
public class MyTextBlock : System.Windows.Controls.TextBlock
{
protected override void OnToolTipOpening(WinControls.ToolTipEventArgs e)
{
if (TextTrimming != TextTrimming.None)
{
e.Handled = !IsTextTrimmed();
}
}
private bool IsTextTrimmed()
{
Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
return ActualWidth < DesiredSize.Width;
}
}
XAML:
<MyTextBlock Text="{Binding Text}" TextTrimming="CharacterEllipsis" ToolTip="{Binding Text}" />
Expanding on bidy's answer. This will create a TextBlock that only shows the tooltip when not all text is shown. The tooltip will be resized to the contents (as opposed to the default tooltip which will remain a one-line box with the text cut off).
using System;
using System.Windows;
using System.Windows.Controls;
namespace MyComponents
{
public class CustomTextBlock : TextBlock
{
protected override void OnInitialized(EventArgs e)
{
// we want a tooltip that resizes to the contents -- a textblock with TextWrapping.Wrap will do that
var toolTipTextBlock = new TextBlock();
toolTipTextBlock.TextWrapping = TextWrapping.Wrap;
// bind the tooltip text to the current textblock Text binding
var binding = GetBindingExpression(TextProperty);
if (binding != null)
{
toolTipTextBlock.SetBinding(TextProperty, binding.ParentBinding);
}
var toolTipPanel = new StackPanel();
toolTipPanel.Children.Add(toolTipTextBlock);
ToolTip = toolTipPanel;
base.OnInitialized(e);
}
protected override void OnToolTipOpening(ToolTipEventArgs e)
{
if (TextTrimming != TextTrimming.None)
{
e.Handled = !IsTextTrimmed();
}
}
private bool IsTextTrimmed()
{
Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
return ActualWidth < DesiredSize.Width;
}
}
}
XAML usage:
<Window ...
xmlns:components="clr-namespace:MyComponents"
... >
<components:CustomTextBlock Text="{Binding Details}" TextTrimming="CharacterEllipsis" />
I had a small issue using Alex answer and had to change my logic a bit to clarify if the text in the text block was being trimmed.
var formattedText = new FormattedText(
Text, System.Threading.Thread.CurrentThread.CurrentCulture, FlowDirection, typeface, FontSize,
Foreground, VisualTreeHelper.GetDpi( this ).PixelsPerDip ) { MaxTextWidth = ActualWidth };
//Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
return ( Math.Floor(formattedText.Height ) > ActualHeight || Math.Floor( formattedText.MinWidth ) > ActualWidth;
This works perfectly for me.
I defined a user control that was a TextBlock with ellipsis enabled. Then I defined 2 functions for OnMouseUp and OnMouseDown, so that when the user clicked on the textblock that had overflow it would display a tooltip with the full value.
This is the OnMouseDown function
private void TextBlockWithToolTipView_OnMouseDown(
object sender,
MouseButtonEventArgs e )
{
var typeface = new Typeface(
FontFamily,
FontStyle,
FontWeight,
FontStretch);
var formattedText = new FormattedText(
Text, System.Threading.Thread.CurrentThread.CurrentCulture, FlowDirection, typeface, FontSize,
Foreground, VisualTreeHelper.GetDpi( this ).PixelsPerDip ) { MaxTextWidth = ActualWidth };
if (Math.Floor(formattedText.Height) > ActualHeight || Math.Floor(formattedText.MinWidth) > ActualWidth )
{
if( ToolTip is ToolTip tt )
{
{
if( tt.PlacementTarget == null )
{
tt.PlacementTarget = this;
}
tt.IsOpen = true;
e.Handled = true;
}
}
}
}
And this was the Xaml bit
<TextBlock
ToolTipService.IsEnabled="True"
MouseDown="TextBlockWithToolTipView_OnMouseDown"
MouseLeave="TextBlockWithToolTipView_OnMouseLeave"
TextTrimming="CharacterEllipsis"
TextWrapping="WrapWithOverflow">
<TextBlock.ToolTip>
<ToolTip
DataContext="{Binding Path=PlacementTarget, RelativeSource={x:Static RelativeSource.Self}}">
<TextBlock Text="{Binding Path=Text, Mode=OneWay }"
TextWrapping="Wrap"/>
</ToolTip>
</TextBlock.ToolTip>
</TextBlock>
Related
I want to create a texbox that will have a directory/file path inside it. If directory path is too long the text should appear to be trimmed with ellipsis, I would like ellipsis to appear in the middle of the path string, for example, D:\Directory1\Directory2\Directory3 can be trimmed as D:\...\Directory3.
The path itself should be bound to a ViewModel so it can be used in MVVM model.
I've encountered this issue recently, so I decided to share my solution to it here.
First of all inspired by this thread How to create a file path Trimming TextBlock with Ellipsis I decided to create my custom TextBlock that will trim its text with ellipsis, this the implementation, I wrote comments so that the code is clear:
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace PathTrimming.Controls
{
public class PathTrimmingTextBlock : TextBlock, INotifyPropertyChanged
{
#region Dependency properties
//This property represents the Text of this textblock that can be bound to another viewmodel property,
//whenever this property is updated the Text property will be updated too.
//We cannot bind to Text property directly because once we update Text, e.g., Text = "NewValue", the binding will be broken
public string BoundedText
{
get { return GetValue(BoundedTextProperty).ToString(); }
set { SetValue(BoundedTextProperty, value); }
}
public static readonly DependencyProperty BoundedTextProperty = DependencyProperty.Register(
nameof(BoundedText), typeof(string), typeof(PathTrimmingTextBlock),
new PropertyMetadata(string.Empty, new PropertyChangedCallback(BoundedTextProperty_Changed)));
//Every time the property BoundedText is updated two things should be done:
//1) Text should be updated to be equal to new BoundedText
//2) New path should be trimmed again
private static void BoundedTextProperty_Changed(object sender, DependencyPropertyChangedEventArgs e)
{
var pathTrimmingTextBlock = (PathTrimmingTextBlock)sender;
pathTrimmingTextBlock.OnPropertyChanged(nameof(BoundedText));
pathTrimmingTextBlock.Text = pathTrimmingTextBlock.BoundedText;
pathTrimmingTextBlock.TrimPathAsync();
}
#endregion
private const string Ellipsis = "...";
public PathTrimmingTextBlock()
{
// This will make sure if the directory name is too long it will be trimmed with ellipsis on the right side
TextTrimming = TextTrimming.CharacterEllipsis;
//setting the event handler for every time this PathTrimmingTextBlock is rendered
Loaded += new RoutedEventHandler(PathTrimmingTextBox_Loaded);
}
private void PathTrimmingTextBox_Loaded(object sender, RoutedEventArgs e)
{
//asynchronously update Text, so that the window won't be frozen
TrimPathAsync();
}
private void TrimPathAsync()
{
Task.Run(() => Dispatcher.Invoke(() => TrimPath()));
}
private void TrimPath()
{
var isWidthOk = false; //represents if the width of the Text is short enough and should not be trimmed
var widthChanged = false; //represents if the width of Text was changed, if the text is short enough at the begging it should not be trimmed
var wasTrimmed = false; //represents if Text was trimmed at least one time
//in this loop we will be checking the current width of textblock using FormattedText at every iteration,
//if the width is not short enough to fit textblock it will be shrinked by one character, and so on untill it fits
do
{
//widthChanged? Text + Ellipsis : Text - at first iteration we have to check if Text is not already short enough to fit textblock,
//after widthChanged = true, we will have to measure the width of Text + Ellipsis, because ellipsis will be added to Text
var formattedText = new FormattedText(widthChanged ? Text + Ellipsis : Text,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
FontSize,
Foreground);
//check if width fits textblock RenderSize.Width, (cannot use Width here because it's not set during rendering,
//and cannot use ActualWidth either because it is the initial width of Text not textblock itself)
isWidthOk = formattedText.Width < RenderSize.Width;
//if it doesn't fit trim it by one character
if (!isWidthOk)
{
wasTrimmed = TrimPathByOneChar();
widthChanged = true;
}
//continue loop
} while (!isWidthOk && wasTrimmed);
//Format Text with ellipsis, if width was changed (after previous loop we may have gotten a path like this "D:\Dire\Directory"
//it should be formatted to "D:\...\Directory")
if (widthChanged)
{
FormatWithEllipsis();
}
}
//Trim Text by one character before last slash, if Text doesn't have slashes it won't be trimmed with ellipsis in the middle,
//instead it will be trimmed with ellipsis at the end due to having TextTrimming = TextTrimming.CharacterEllipsis; in the constructor
private bool TrimPathByOneChar()
{
var lastSlashIndex = Text.LastIndexOf('\\');
if (lastSlashIndex > 0)
{
Text = Text.Substring(0, lastSlashIndex - 1) + Text.Substring(lastSlashIndex);
return true;
}
return false;
}
//"\Directory will become "...\Directory"
//"Dire\Directory will become "...\Directory"\
//"D:\Dire\Directory" will become "D:\...\Directory"
private void FormatWithEllipsis()
{
var lastSlashIndex = Text.LastIndexOf('\\');
if (lastSlashIndex == 0)
{
Text = Ellipsis + Text;
}
else if (lastSlashIndex > 0)
{
var secondastSlashIndex = Text.LastIndexOf('\\', lastSlashIndex - 1);
if (secondastSlashIndex < 0)
{
Text = Ellipsis + Text.Substring(lastSlashIndex);
}
else
{
Text = Text.Substring(0, secondastSlashIndex + 1) + Ellipsis + Text.Substring(lastSlashIndex);
}
}
}
//starndard implementation of INotifyPropertyChanged to be able to notify BoundedText property change
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
Now after having the texblock we created we have to somehow "wire" it to TextBox in XAML, it can be done using ControlTemplate. This is the full XAML code, again I wrote comments so it should be easy to follow along:
<Window x:Class="PathTrimming.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewmodel = "clr-namespace:PathTrimming.ViewModel"
xmlns:controls="clr-namespace:PathTrimming.Controls"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<!-- Assigning datacontext to the window -->
<Window.DataContext>
<viewmodel:MainViewModel/>
</Window.DataContext>
<Window.Resources>
<ResourceDictionary>
<!--This is the most important part, if TextBox is not in focused,
it will be rendered as PathTrimmingTextBlock,
if it is focused it shouldn't be trimmed and will be rendered as default textbox.
To achieve this I'm using DataTrigger and ControlTemplate-->
<Style x:Key="TextBoxDefaultStyle" TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsKeyboardFocused, RelativeSource={RelativeSource Self}}" Value="False">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border
BorderThickness="1"
BorderBrush="#000">
<controls:PathTrimmingTextBlock BoundedText="{TemplateBinding Text}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</Window.Resources>
<!--Grid with two textboxes and button that updates the textboxes with new pathes from a random path pool-->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox Grid.Row="0" Grid.Column="0" Width="100" Text="{Binding Path1}" Style="{StaticResource TextBoxDefaultStyle}"/>
<TextBox Grid.Row="1" Grid.Column="0" Width="100" Text="{Binding Path2}" Style="{StaticResource TextBoxDefaultStyle}"/>
<Button Grid.Row="2" Content="Update pathes" Command="{Binding UpdatePathesCmd}"/>
</Grid>
</Window>
Now finally what is left, is to write our ViewModel that is responsible to feed data to the View. Here I utilized MVVM Light library to simplify the code, but this is not important, using any other approach should work fine.
This is the code with comments, should be pretty self explanatory anyway:
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System;
using System.Windows.Input;
namespace PathTrimming.ViewModel
{
public class MainViewModel : ViewModelBase
{
public string Path1
{
get { return _path1; }
set
{
_path1 = value;
RaisePropertyChanged();
}
}
public string Path2
{
get { return _path2; }
set
{
_path2 = value;
RaisePropertyChanged();
}
}
private string _path1;
private string _path2;
public MainViewModel()
{
UpdatePathes();
}
//The command that will update Path1 and Path2 with some random path values
public ICommand UpdatePathesCmd
{
get { return new RelayCommand(UpdatePathes); }
}
private void UpdatePathes()
{
Path1 = PathProvider.GetPath();
Path2 = PathProvider.GetPath();
}
}
//A simple static class to provide a pool of different pathes
public static class PathProvider
{
private static Random randIndexGenerator = new Random();
private static readonly string[] pathes =
{
"D:\\Directory1\\Directory2\\Directory3",
"D:\\Directory1\\Directory2",
"Directory1\\Directory2\\Directory3",
"D:\\Directory1\\Directory12345678901234567890",
"Directory1234567890123456789012345678901234567890",
"D:\\Directory1"
};
public static string GetPath()
{
var randIndex = randIndexGenerator.Next(pathes.Length);
return pathes[randIndex];
}
}
}
I have a DataGrid on a WPF page and want to prevent the user from selecting cells. As this feature is needed just for testing, I don't want to change everything in code.
After my DataGrid is filled, I make sure all of its rows are selected. Now I want to make sure that user cannot select/unselect rows.
I tried setting IsEnabled = false and IsHitTestVisible = "False" but both of these solutions disable scrollbars.
Is there any way to do this?
Why not just set IsHitTestVisible="False" for your DataGridRow or DataGridCell objects only?
That's easy to do using an implicit style in the <DataGrid.Resources>, and should only disable hit-testing on the rows or cells, which should leave the other areas of the DataGrid functional, such as the Headers or ScrollBars
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridRow}">
<Setter Property="IsHitTestVisible" Value="False" />
</Style>
</DataGrid.Resources>
You have two choices:
You disable selection in style (in this case you turn off only color in style, but physically SelectedItem or SelectedItems will change). You can easily find out how you can turn off selection style.
You can disable changing selection without changing SelectedItem or SelectedItems (in this case your selection style will not change too).
In WPF i don't like to override standard controls. So, we need a Behavior:
public class DisableSelectionDataGridBehavior : Behavior<DataGrid>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.PreviewMouseLeftButtonDown += AssociatedObjectOnPreviewMouseLeftButtonDown;
}
private void AssociatedObjectOnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var dependencyObject = AssociatedObject.InputHitTest(e.GetPosition(AssociatedObject)) as DependencyObject;
if (dependencyObject == null) return;
var elements = dependencyObject.GetParents().OfType<FrameworkElement>().Where(DataGridCellExtended.GetIsDisableSelection).ToList();
if (!elements.Any()) return;
e.Handled = true;
var args = new MouseButtonEventArgs(e.MouseDevice, e.Timestamp, e.ChangedButton, e.StylusDevice);
args.RoutedEvent = UIElement.MouseLeftButtonDownEvent;
args.Source = e.Source;
elements.ForEach(item =>
{
item.RaiseEvent(args);
var children = item.GetChildren<FrameworkElement>();
children.ForEach(child => child.RaiseEvent(args));
});
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.PreviewMouseLeftButtonDown -= AssociatedObjectOnPreviewMouseLeftButtonDown;
}
}
Second, you need an Extended class:
public class DataGridCellExtended
{
public static readonly DependencyProperty IsDisableSelectionProperty = DependencyProperty.RegisterAttached("IsDisableSelection", typeof(Boolean), typeof(DataGridCellExtended));
public static Boolean GetIsDisableSelection(DependencyObject o)
{
return (Boolean)o.GetValue(IsDisableSelectionProperty);
}
public static void SetIsDisableSelection(DependencyObject o, Boolean value)
{
o.SetValue(IsDisableSelectionProperty, value);
}
}
And finally in XAML you need something like this:
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="{x:Type items:YourViewModel}">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Button Margin="0"
extends:DataGridCellExtended.IsDisableSelection="True">
<Path Data="M5,0L3,2 1,0 0,1 2,3 0,5 1,6 3,4 5,6 6,5 4,3 6,1z"
Fill="{Binding Foreground, RelativeSource={RelativeSource FindAncestor, AncestorType=DataGridCell}}"
Width="12"
Height="12"
Stretch="Uniform"/>
</Button>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
You can write your logic for extended class.
public static IEnumerable<DependencyObject> GetParents(this DependencyObject element)
{
if (element != null)
{
while (true)
{
var parent = element.GetParent();
var dependencyObject = parent;
element = parent;
if (dependencyObject == null)
{
break;
}
yield return element;
}
yield break;
}
else
{
throw new ArgumentNullException("element");
}
}
private static IEnumerable<DependencyObject> GetChildrenRecursive(this DependencyObject element)
{
if (element != null)
{
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
{
var dependencyObject = VisualTreeHelper.GetChild(element, i);
yield return dependencyObject;
foreach (var childrenRecursive in dependencyObject.GetChildrenRecursive())
{
yield return childrenRecursive;
}
}
}
else
{
throw new ArgumentNullException("element");
}
}
I have a listview. I have set following int that :-
<ListView KeyboardNavigation.TabNavigation="Local" SelectionMode="Extended">
<ListView.ItemContainerStyle>
<Style>
<Setter Property="KeyboardNavigation.IsTabStop" Value="False"/>
</Style>
</ListView.ItemContainerStyle>
One column in listview contains TextBox's.
If I set the UpdateSourceTrigger=LostFocus
in my textbox, I can not tab through the listview...Instead if I set UpdateSourceTrigger=Explicit, the tabbing is working...but source will not get updated.
Please help me
EDIT
public class TextBoxBehavior
{
#region Attached Property EscapeClearsText
public static readonly DependencyProperty EscapeClearsTextProperty
= DependencyProperty.RegisterAttached("EscapeClearsText", typeof(bool), typeof(TextBoxBehavior),
new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnEscapeClearsTextChanged)));
private static void OnEscapeClearsTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
{
var textBox = d as TextBox;
if (textBox != null)
{
textBox.KeyUp -= TextBoxKeyUp;
textBox.KeyUp += TextBoxKeyUp;
}
}
}
private static void TextBoxKeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
//((DataContext<string>)((TextBox)sender).GetBindingExpression(TextBox.TextProperty).DataItem).RollbackChanges();
((TextBox)sender).Text = string.Empty;
}
else if (e.Key == Key.Enter)
{
((TextBox)sender).GetBindingExpression(TextBox.TextProperty).UpdateSource();
}
}
public static void SetEscapeClearsText(DependencyObject dependencyObject, bool escapeClearsText)
{
if (!ReferenceEquals(null, dependencyObject))
dependencyObject.SetValue(EscapeClearsTextProperty, escapeClearsText);
}
public static bool GetEscapeClearsText(DependencyObject dependencyObject)
{
if (!ReferenceEquals(null, dependencyObject))
return (bool)dependencyObject.GetValue(EscapeClearsTextProperty);
return false;
}
#endregion Attached Property EscapeClearsText
}
Below is the listview/gridview column which has the attached property in it.
<GridViewColumn Width="60">
<GridViewColumnHeader Content="Priority"
Command="{Binding Path=SortSelectedClaimCodeGroupsCommand}"
CommandParameter="Item.IntPriority">
</GridViewColumnHeader>
<GridViewColumn.CellTemplate>
<DataTemplate>
<Border DataContext="{Binding Item.Priority}"
Style="{StaticResource ValidationResultBorderStyle}" HorizontalAlignment="Left" >
<TextBox Width="200" MaxLength="25" Text="{Binding Path=Value,Mode=TwoWay,
UpdateSourceTrigger=Explicit}" local:TextBoxBehavior.EscapeClearsText="True" >
When you set the UpdateSourceTrigger as explicit, you have to update the source by explicitly calling the method UpdateSource on your BindingExpression. Where is the code for that?
EDIT
In your TextBoxKeyUp event you are overwriting your Binding by setting the text on the press of Escape key. Firstly you bind it to the property Value and later you are explicitly setting the Textbox text property to String.Empty.This way text property will loose it's binding. So, later whenever you call UpdateSource it won't propagate to Source value since it's no longer binded to the Text property of textbox. Instead you should set the text like this -
((TextBox)sender).SetCurrentValue(TextBox.TextProperty, String.Empty);
This way your binding will be preserved and UpdateSource would work as it should.
I'm using Silverlight on Windows Phone 7.
I want to display the first part of some text in a TextBlock in bold, and the rest in normal font. The complete text must wrap. I want the bolded part to contain text from one property in my ViewModel, and the plain text to contain text from a different property.
The TextBlock is defined in a DataTemplate associated with a LongListSelector.
My initial attempt was:
<TextBlock TextWrapping="Wrap">
<TextBlock.Inlines>
<Run Text="{Binding Property1}" FontWeight="Bold"/>
<Run Text="{Binding Property2}"/>
</TextBlock.Inlines>
</TextBlock>
This fails at runtime with the spectacularly unhelpful "AG_E_RUNTIME_MANAGED_UNKNOWN_ERROR". This is a known issue because the Run element is not a FrameworkElement and cannot be bound.
My next attempt was to put placeholders in place, and then update them in code:
<TextBlock Loaded="TextBlockLoaded" TextWrapping="Wrap">
<TextBlock.Inlines>
<Run FontWeight="Bold">Placeholder1</Run>
<Run>Placeholder2</Run>
</TextBlock.Inlines>
</TextBlock>
In the code-behind (yes I am desparate!):
private void TextBlockLoaded(object sender, RoutedEventArgs e)
{
var textBlock = (TextBlock)sender;
var viewModel = (ViewModel)textBlock.DataContext;
var prop1Run = (Run)textBlock.Inlines[0];
var prop2Run = (Run)textBlock.Inlines[1];
prop1Run.Text = viewModel.Property1;
prop2Run.Text = viewModel.Property2;
}
This seemed to work, but because I am using the LongListSelector, although items get recycled, the Loaded codebehind event handler doesn't re-initialize the Runs, so very quickly the wrong text is displayed...
I've looked at using the LongListSelector's Linked event (which I already use to free up images that I display in the list), but I can't see how I can use that to re-initialize the Runs' text properties.
Any help appreciated!
I finally found a solution that works for me.
As I mention in the comment, Paul Stovell's approach would not work.
Instead I used a similar approach to add an attached property to the TextBlock, bound to the TextBlock's DataContext, and attached properties on the runs, indicating which ViewModel properties they should be bound to:
<TextBlock TextWrapping="Wrap"
Views:BindableRuns.Target="{Binding}">
<TextBlock.Inlines>
<Run FontWeight="Bold" Views:BindableRuns.Target="Property1"/>
<Run Views:BindableRuns.Target="Property2"/>
</TextBlock.Inlines>
</TextBlock>
Then in my attached TextBox Target (datacontext) property's changed event, I update the Runs, and subscribe to be notified of changes to the TextBox Target properties. When a TextBox Target property changes, I updated any associated Run's text accordingly.
public static class BindableRuns
{
private static readonly Dictionary<INotifyPropertyChanged, PropertyChangedHandler>
Handlers = new Dictionary<INotifyPropertyChanged, PropertyChangedHandler>();
private static void TargetPropertyPropertyChanged(
DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs e)
{
if(!(dependencyObject is TextBlock)) return;
var textBlock = (TextBlock)dependencyObject;
AddHandler(e.NewValue as INotifyPropertyChanged, textBlock);
RemoveHandler(e.OldValue as INotifyPropertyChanged);
InitializeRuns(textBlock, e.NewValue);
}
private static void AddHandler(INotifyPropertyChanged dataContext,
TextBlock textBlock)
{
if (dataContext == null) return;
var propertyChangedHandler = new PropertyChangedHandler(textBlock);
dataContext.PropertyChanged += propertyChangedHandler.PropertyChanged;
Handlers[dataContext] = propertyChangedHandler;
}
private static void RemoveHandler(INotifyPropertyChanged dataContext)
{
if (dataContext == null || !Handlers.ContainsKey(dataContext)) return;
dataContext.PropertyChanged -= Handlers[dataContext].PropertyChanged;
Handlers.Remove(dataContext);
}
private static void InitializeRuns(TextBlock textBlock, object dataContext)
{
if (dataContext == null) return;
var runs = from run in textBlock.Inlines.OfType<Run>()
let propertyName = (string)run.GetValue(TargetProperty)
where propertyName != null
select new { Run = run, PropertyName = propertyName };
foreach (var run in runs)
{
var property = dataContext.GetType().GetProperty(run.PropertyName);
run.Run.Text = (string)property.GetValue(dataContext, null);
}
}
private class PropertyChangedHandler
{
private readonly TextBlock _textBlock;
public PropertyChangedHandler(TextBlock textBlock)
{
_textBlock = textBlock;
}
public void PropertyChanged(object sender,
PropertyChangedEventArgs propertyChangedArgs)
{
var propertyName = propertyChangedArgs.PropertyName;
var run = _textBlock.Inlines.OfType<Run>()
.Where(r => (string) r.GetValue(TargetProperty) == propertyName)
.SingleOrDefault();
if(run == null) return;
var property = sender.GetType().GetProperty(propertyName);
run.Text = (string)property.GetValue(sender, null);
}
}
public static object GetTarget(DependencyObject obj)
{
return obj.GetValue(TargetProperty);
}
public static void SetTarget(DependencyObject obj,
object value)
{
obj.SetValue(TargetProperty, value);
}
public static readonly DependencyProperty TargetProperty =
DependencyProperty.RegisterAttached("Target",
typeof(object),
typeof(BindableRuns),
new PropertyMetadata(null,
TargetPropertyPropertyChanged));
}
I suggest you give the BindableRun a try. I've only used it in WPF, but I don't see why it wouldn't work in Silverlight.
I want to increase or decrease font size of controls such as window, treeView, ribbon menu etc that are contained by main window.
I have a font size slider create method and I want to acces all of Control and TextBlock by using visualtree helper and increase or decrease their font size according to slider value.
Methods are below;
private StackPanel CreateFontSizeSlider()
{
StackPanel fontSizePanel = new StackPanel();
fontSizePanel.Orientation = Orientation.Horizontal;
Slider fontSizeSlider = new Slider();
fontSizeSlider.Minimum = -3;
fontSizeSlider.Maximum = 5;
fontSizeSlider.Value = 0;
fontSizeSlider.Orientation = Orientation.Horizontal;
fontSizeSlider.TickPlacement = System.Windows.Controls.Primitives.TickPlacement.TopLeft;
fontSizeSlider.IsSnapToTickEnabled = true;
fontSizeSlider.ValueChanged += new RoutedPropertyChangedEventHandler<double>(fontSizeSlider_ValueChanged);
fontSizeSlider.Width = 150;
fontSizePanel.Children.Add(fontSizeSlider);
return fontSizePanel;
}
public static void ChangeControlsFontSize(DependencyObject dependencyObject, double value)
{
foreach (DependencyObject childItem in GetChildren(dependencyObject))
{
if (childItem is Control)
{
Control control = childItem as Control;
control.FontSize = control.FontSize + value;
}
else if (childItem is TextBlock)
{
((TextBlock)childItem).FontSize = ((TextBlock)childItem).FontSize + value;
}
ChangeControlsFontSize(childItem, value);
}
}
private static IEnumerable<DependencyObject> GetChildren(DependencyObject reference)
{
int childCount = VisualTreeHelper.GetChildrenCount(reference);
for (int i = 0; i < childCount; i++)
{
yield return VisualTreeHelper.GetChild(reference, i);
}
}
private void fontSizeSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
ChangeControlsFontSize(this, e.NewValue - e.OldValue);
}
There are some problems;
Firstly I can not acces all controls by walking visual tree. For example I cannot acces closed ribbon menu items. So I can not change their fonts.
Secondly some controls contain another controls so I increase or decrease control font size twice.
Is there any solution for these proplems or is there another way to do this ? Could you help me please ?
You are working way too hard. :-D
Create a style like this:
<Style TargetType="ListBox">
<Setter Property="FontFamily" Value="Tahoma"/>
<Setter Property="FontSize">
<Setter.Value>
<Binding Source="{x:Static Application.Current}" Path="fontSize"/>
</Setter.Value>
</Setter>
</Style>
Create a property called fontSize on your application.
Make a slider like this:
<Slider Name="fontSize" Minimum="10" Maximum="22" IsSnapToTickEnabled="True" TickPlacement="TopLeft"
Value="{Binding Source={x:Static Application.Current}, Path=fontSize, Mode=TwoWay}"/>
Now, any control with that style will nicely resize - and no code is needed!