<TextBlock Width="100" Text="The quick brown fox jumps over the lazy dog" TextTrimming="WordEllipsis">
<TextBlock.ToolTip>
<ToolTip DataContext="{Binding Path=PlacementTarget, RelativeSource={x:Static RelativeSource.Self}}">
<TextBlock Text="{Binding Text}"/>
</ToolTip>
</TextBlock.ToolTip>
</TextBlock>
How can I show the ToolTip only when the text is trimmed? Like the windows desktp shortcut icons.
Working off of Eyjafj...whatever's idea, I arrived at a working, mostly declarative solution that at least doesn't require a custom control. The first hurdle to overcome is getting at the TextBlock. Because the ToolTip is rendered outside of the visual tree, you can't use a RelativeSource binding or ElementName to get at the TextBlock. Luckily, the ToolTip class provides a reference to its related element via the PlacementTarget property. So you can bind the ToolTip's Visibility property to the ToolTip itself and use its PlacementTarget property to access properties of the TextBlock:
<ToolTip Visibility="{Binding RelativeSource={RelativeSource Self}, Path=PlacementTarget, Converter={StaticResource trimmedVisibilityConverter}}">
The next step is using a converter to look at the TextBlock we've bound to to determine if the ToolTip should be visible or not. You can do this using the ActualWidth and the DesiredSize. ActualWidth is exactly what it sounds like; the width your TextBlock has been rendered to on the screen. DesiredSize is the width your TextBlock would prefer to be. The only problem is, DesiredSize seems to take the TextTrimming into account and does not give you the width of the full, untrimmed text. To solve this, we can re-call the Measure method passing Double.Positive infinity to, in effect, ask how wide the TextBlock would be if it its width were not constrained. This updates the DesiredSize property and then we can do the comparison:
textBlock.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
if (((FrameworkElement)value).ActualWidth < ((FrameworkElement)value).DesiredSize.Width)
return Visibility.Visible;
This approach is actually illustrated here as an attached behavior if you want to apply it automatically to TextBlocks or don't want to waste resources on creating ToolTips that will always be invisible. Here is the full code for my example:
The Converter:
public class TrimmedTextBlockVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value == null) return Visibility.Collapsed;
FrameworkElement textBlock = (FrameworkElement)value;
textBlock.Measure(new System.Windows.Size(Double.PositiveInfinity, Double.PositiveInfinity));
if (((FrameworkElement)value).ActualWidth < ((FrameworkElement)value).DesiredSize.Width)
return Visibility.Visible;
else
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
The XAML:
<UserControl.Resources>
<local:TrimmedTextBlockVisibilityConverter x:Key="trimmedVisibilityConverter" />
</UserControl.Resources>
....
<TextBlock TextTrimming="CharacterEllipsis" Text="{Binding SomeTextProperty}">
<TextBlock.ToolTip>
<ToolTip Visibility="{Binding RelativeSource={RelativeSource Self}, Path=PlacementTarget, Converter={StaticResource trimmedVisibilityConverter}}">
<ToolTip.Content>
<TextBlock Text="{Binding SomeTextProperty}"/>
</ToolTip.Content>
</ToolTip>
</TextBlock.ToolTip>
</TextBlock>
I found the simplest solution to extend TextBlock and compare text lengths to determine whether to show tooltip, i.e.,
public class ToolTipTextBlock : TextBlock
{
protected override void OnToolTipOpening(ToolTipEventArgs e)
{
if (TextTrimming != TextTrimming.None)
{
e.Handled = !IsTextTrimmed();
}
}
private bool IsTextTrimmed()
{
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var formattedText = new FormattedText(Text, CultureInfo.CurrentCulture, FlowDirection, typeface, FontSize, Foreground);
return formattedText.Width > ActualWidth;
}
}
Then simply use this custom text block in xaml as follows:
<local:ToolTipTextBlock Text="This is some text that I'd like to show tooltip for!"
TextTrimming="CharacterEllipsis"
ToolTip="{Binding Text,RelativeSource={RelativeSource Self}}"
MaxWidth="10"/>
Based on ideas on this page and with additional algorithmic corrections from another answer I made this very portable class that can be used very easily. Its purpose is to enable trimming and show a ToolTip over the TextBlock when the text is trimmed, like it is known from many applications.
The trimming detection has proven to be precise in my application. The tool tip is shown exactly when the trimming ellipsis is shown.
XAML usage
<!-- xmlns:ui="clr-namespace:Unclassified.UI" -->
<TextBlock Text="Demo" ui:TextBlockAutoToolTip.Enabled="True"/>
C# usage
var textBlock = new TextBlock { Text = "Demo" };
TextBlockAutoToolTip.SetEnabled(textBlock, true);
The complete class
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace Unclassified.UI
{
/// <summary>
/// Shows a ToolTip over a TextBlock when its text is trimmed.
/// </summary>
public class TextBlockAutoToolTip
{
/// <summary>
/// The Enabled attached property.
/// </summary>
public static readonly DependencyProperty EnabledProperty = DependencyProperty.RegisterAttached(
"Enabled",
typeof(bool),
typeof(TextBlockAutoToolTip),
new FrameworkPropertyMetadata(new PropertyChangedCallback(OnAutoToolTipEnabledChanged)));
/// <summary>
/// Sets the Enabled attached property on a TextBlock control.
/// </summary>
/// <param name="dependencyObject">The TextBlock control.</param>
/// <param name="enabled">The value.</param>
public static void SetEnabled(DependencyObject dependencyObject, bool enabled)
{
dependencyObject.SetValue(EnabledProperty, enabled);
}
private static readonly TrimmedTextBlockVisibilityConverter ttbvc = new TrimmedTextBlockVisibilityConverter();
private static void OnAutoToolTipEnabledChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
{
TextBlock textBlock = dependencyObject as TextBlock;
if (textBlock != null)
{
bool enabled = (bool)args.NewValue;
if (enabled)
{
var toolTip = new ToolTip
{
Placement = System.Windows.Controls.Primitives.PlacementMode.Relative,
VerticalOffset = -3,
HorizontalOffset = -5,
Padding = new Thickness(4, 2, 4, 2),
Background = Brushes.White
};
toolTip.SetBinding(UIElement.VisibilityProperty, new System.Windows.Data.Binding
{
RelativeSource = new System.Windows.Data.RelativeSource(System.Windows.Data.RelativeSourceMode.Self),
Path = new PropertyPath("PlacementTarget"),
Converter = ttbvc
});
toolTip.SetBinding(ContentControl.ContentProperty, new System.Windows.Data.Binding
{
RelativeSource = new System.Windows.Data.RelativeSource(System.Windows.Data.RelativeSourceMode.Self),
Path = new PropertyPath("PlacementTarget.Text")
});
toolTip.SetBinding(Control.ForegroundProperty, new System.Windows.Data.Binding
{
RelativeSource = new System.Windows.Data.RelativeSource(System.Windows.Data.RelativeSourceMode.Self),
Path = new PropertyPath("PlacementTarget.Foreground")
});
textBlock.ToolTip = toolTip;
textBlock.TextTrimming = TextTrimming.CharacterEllipsis;
}
}
}
private class TrimmedTextBlockVisibilityConverter : IValueConverter
{
// Source 1: https://stackoverflow.com/a/21863054
// Source 2: https://stackoverflow.com/a/25436070
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var textBlock = value as TextBlock;
if (textBlock == null)
return Visibility.Collapsed;
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,
VisualTreeHelper.GetDpi(textBlock).PixelsPerDip);
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 width 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.
bool isTrimmed = formattedText.Height > textBlock.ActualHeight ||
formattedText.MinWidth > formattedText.MaxTextWidth;
return isTrimmed ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
}
Behaviors are love, behaviors are life.
public class TextBlockAutoToolTipBehavior : Behavior<TextBlock>
{
private ToolTip _toolTip;
protected override void OnAttached()
{
base.OnAttached();
_toolTip = new ToolTip
{
Placement = PlacementMode.Relative,
VerticalOffset = 0,
HorizontalOffset = 0
};
ToolTipService.SetShowDuration(_toolTip, int.MaxValue);
_toolTip.SetBinding(ContentControl.ContentProperty, new Binding
{
Path = new PropertyPath("Text"),
Source = AssociatedObject
});
AssociatedObject.TextTrimming = TextTrimming.CharacterEllipsis;
AssociatedObject.AddValueChanged(TextBlock.TextProperty, TextBlockOnTextChanged);
AssociatedObject.SizeChanged += AssociatedObjectOnSizeChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.RemoveValueChanged(TextBlock.TextProperty, TextBlockOnTextChanged);
AssociatedObject.SizeChanged -= AssociatedObjectOnSizeChanged;
}
private void AssociatedObjectOnSizeChanged(object sender, SizeChangedEventArgs sizeChangedEventArgs)
{
CheckToolTipVisibility();
}
private void TextBlockOnTextChanged(object sender, EventArgs eventArgs)
{
CheckToolTipVisibility();
}
private void CheckToolTipVisibility()
{
if (AssociatedObject.ActualWidth == 0)
Dispatcher.BeginInvoke(
new Action(
() => AssociatedObject.ToolTip = CalculateIsTextTrimmed(AssociatedObject) ? _toolTip : null),
DispatcherPriority.Loaded);
else
AssociatedObject.ToolTip = CalculateIsTextTrimmed(AssociatedObject) ? _toolTip : null;
}
//Source: https://stackoverflow.com/questions/1041820/how-can-i-determine-if-my-textblock-text-is-being-trimmed
private static bool CalculateIsTextTrimmed(TextBlock 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) {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 width 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);
}
}
Usage:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:behavior="clr-namespace:MyWpfApplication.Behavior"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity">
<TextBlock Text="{Binding Text}">
<i:Interaction.Behaviors>
<behavior:TextBlockAutoToolTipBehavior />
</i:Interaction.Behaviors>
</TextBlock>
</Window>
The needed extension methods:
public static class UITools
{
public static void AddValueChanged<T>(this T obj, DependencyProperty property, EventHandler handler)
where T : DependencyObject
{
var desc = DependencyPropertyDescriptor.FromProperty(property, typeof (T));
desc.AddValueChanged(obj, handler);
}
public static void RemoveValueChanged<T>(this T obj, DependencyProperty property, EventHandler handler)
where T : DependencyObject
{
var desc = DependencyPropertyDescriptor.FromProperty(property, typeof (T));
desc.RemoveValueChanged(obj, handler);
}
}
I think that you can create a converter that compares between the ActualWidth of textblock and it's DesiredSize.Width, and return Visibility.
Posted an alternative answer with attached property here, which I think is nicer than using a converter or a derived TextBlock control.
I used this from #pogosoma but with the CalculateIsTextTrimmed function from #snicker which is perfect
private static void SetTooltipBasedOnTrimmingState(TextBlock tb)
{
Typeface typeface = new Typeface(tb.FontFamily, tb.FontStyle, tb.FontWeight, tb.FontStretch);
FormattedText formattedText = new FormattedText(tb.Text, System.Threading.Thread.CurrentThread.CurrentCulture, tb.FlowDirection, typeface, tb.FontSize, tb.Foreground)
{ MaxTextWidth = tb.ActualWidth };
bool isTextTrimmed = (formattedText.Height > tb.ActualHeight || formattedText.MinWidth > formattedText.MaxTextWidth);
ToolTipService.SetToolTip(tb, isTextTrimmed ? tb.ToolTip : null);
}
Related
I want to show in my C#-WPF application a text containing links. The texts are static and known during compile time.
The following is doing want i want when working directly on the XAML file:
<TextBlock Name="TextBlockWithHyperlink">
Some text
<Hyperlink
NavigateUri="http://somesite.com"
RequestNavigate="Hyperlink_RequestNavigate">
some site
</Hyperlink>
some more text
</TextBlock>
Since using MVVM i want to bind the Textblock to a newly constructed Textblock object, through a dependency property. The XAML then looks like this:
<StackPanel Grid.Row="1" Margin="5 0 0 0">
<TextBlock Height="16" FontWeight="Bold" Text="Generic Text with link"/>
<TextBlock Text="{Binding Path=TextWithLink, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
In my ViewModel i place
private void someMethod(){
...
TextWithLink = CreateText();
...
}
private TextBlock(){
TextBlock tb = new TextBlock();
Run run1 = new Run("Text preceeding the hyperlink.");
Run run2 = new Run("Text following the hyperlink.");
Run run3 = new Run("Link Text.");
Hyperlink hyperl = new Hyperlink(run3);
hyperl.NavigateUri = new Uri("http://search.msn.com");
tb.Inlines.Add(run1);
tb.Inlines.Add(hyperl);
tb.Inlines.Add(run2);
return tb;
}
private TextBlock _textWithLink;
public TextBlock TextWithLink {
get => _textWithLink;
set{
_textWithLink = value;
OnPropertyChanged();
}
}
The dependency property setup is working i see a new TextBlock getting assigned to the XAML control, however there is no content shown, just the displayed text reads
System.Windows.Controls.TextBlock
rather than the content. I cannot get my head around what i have to change to show the desired mixed text. Happy for an help.
Instead of using a TextBlock instance in a view model, you should instead use a collection of Inline elements with a UI element that accept it as the source of a Binding.
Since the Inlines property of a TextBlock is not bindable, you may create a deribed TextBlock with a bindable property like this:
public class MyTextBlock : TextBlock
{
public static readonly DependencyProperty BindableInlinesProperty =
DependencyProperty.Register(
nameof(BindableInlines),
typeof(IEnumerable<Inline>),
typeof(MyTextBlock),
new PropertyMetadata(null, BindableInlinesPropertyChanged));
public IEnumerable<Inline> BindableInlines
{
get { return (IEnumerable<Inline>)GetValue(BindableInlinesProperty); }
set { SetValue(BindingGroupProperty, value); }
}
private static void BindableInlinesPropertyChanged(
DependencyObject o, DependencyPropertyChangedEventArgs e)
{
var textblock = (MyTextBlock)o;
var inlines = (IEnumerable<Inline>)e.NewValue;
textblock.Inlines.Clear();
if (inlines != null)
{
textblock.Inlines.AddRange(inlines);
}
}
}
Now you may use it like
<local:MyTextBlock BindableInlines="{Binding SomeInlines}"/>
with a view model property like this:
public IEnumerable<Inline> SomeInlines { get; set; }
...
var link = new Hyperlink(new Run("Search"));
link.NavigateUri = new Uri("http://search.msn.com");
link.RequestNavigate += (s, e) => Process.Start(e.Uri.ToString());
SomeInlines = new List<Inline>
{
new Run("Some text "),
link,
new Run(" and more text")
};
I have a RichTextBox bound to a string.
Using C# I generate a string that writes to it.
But if I want to manually change the text by clicking into the RichTextBox and deleting it with the backspace key, or pressing Enter to make a new line, the binding becomes broken and I can no longer programmatically write to it with the string a second time.
XAML
<RichTextBox x:Name="rtbScriptView"
Margin="11,71,280,56"
Padding="10,10,10,48"
FontSize="14"
Grid.ColumnSpan="1"
VerticalScrollBarVisibility="Auto"
RenderOptions.ClearTypeHint="Enabled"
Style="{DynamicResource RichTextBoxStyle}">
<FlowDocument>
<Paragraph>
<Run Text="{Binding ScriptView_Text,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />
</Paragraph>
</FlowDocument>
</RichTextBox>
View Model
private string _ScriptView_Text;
public string ScriptView_Text
{
get { return _ScriptView_Text; }
set
{
if (_ScriptView_Text == value)
{
return;
}
_ScriptView_Text = value;
OnPropertyChanged("ScriptView_Text");
}
}
C#
ViewModel vm = new ViewModel();
DataContext = vm;
// Display a string in the RichTextBox
vm.ScriptView_Text = "This is a test."; // <-- This won't work if text is manually modified
When you edit the RichTextBox, you alter the elements inside of the FlowDocument element. The element you have a binding on, is probably removed at some point during this editing.
Have a look at RichtTextBox.Document.Groups to see what's happening when you edit the RichTextBox.
The default RichTextBox does not really support MVVM/Binding very well. You'd want to have a binding on the Document property, but this is not supported for the default RichTextBox.
You could have a look here.
Or extend it yourself, something like this?:
BindableRichTextBox class
public class BindableRichTextBox : RichTextBox
{
public static readonly DependencyProperty DocumentProperty = DependencyProperty.Register(nameof(Document), typeof(FlowDocument), typeof(BindableRichTextBox), new FrameworkPropertyMetadata(null, OnDocumentChanged));
public new FlowDocument Document
{
get => (FlowDocument)GetValue(DocumentProperty);
set => SetValue(DocumentProperty, value);
}
public static void OnDocumentChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var rtb = (RichTextBox)obj;
rtb.Document = args.NewValue != null ? (FlowDocument)args.NewValue : new FlowDocument();
}
}
XAML
<controls:BindableRichTextBox Document="{Binding YourFlowDocumentObject, Mode=OneWay}"/>
Then you can get the string from the FlowDocument.
Why you have to write this line. Please remove line after check.
if (_ScriptView_Text == value)
{
return;
}
I am searching all the time for solution and cant get correct one.
I have grid that have width 960 and have ScrollViewer in it. Now i would like to know value (horizontal offset) of my scroll while scrolling. All solutions that i am finding is for wpf/silverlight and it wont works for me.
Edit
Ok, here is the example code, xaml:
<ScrollViewer Name="Scroll" LayoutUpdated="ScrollViewer_LayoutUpdated" IsEnabled="True" Width="480" ScrollViewer.HorizontalScrollBarVisibility="Auto">
<Grid x:Name="ContentPanel" Background="Red" Margin="12,0,12,0" Width="960">
<Rectangle Name="GreenRectangle" Fill="Green" Width="240" Height="240"></Rectangle>
</Grid>
</ScrollViewer>
c#
private void ScrollViewer_LayoutUpdated(object sender, EventArgs e)
{
GreenRectangle.Width = Scroll.HorizontalOffset;
GreenRectangle.Height = Scroll.HorizontalOffset;
}
But the problem is that it is not changing size all the time. Maybe my English is not well and you cant uderstand me. Here is movie example, i am sliding left right and the size is always the same. When i stop sliding it is changing size.
https://www.dropbox.com/s/eh28oavxpsy19bw/20130122_1601_56.avi
It is possible by using the scrollviewers dependency properties, it has a HorizontalOffset and a VerticalOffset. The trick is to bind event to the scrollviewer, but it can bee done in the load event handler. If you put a wide grid in your scrollviewer you can get the offset!
In your xaml file (MainPage sample here):
<ScrollViewer Loaded="ScrollViewer_Loaded_1">
<Grid x:Name="ContentPanel" Grid.Row="1" Width="1000" Margin="12,0,12,0">
<StackPanel>
...
In your code behind file (MainPage.cs here):
public static readonly DependencyProperty ScrollViewVerticalOffsetProperty =
DependencyProperty.Register(
"ScrollViewVerticalOffset",
typeof(double),
typeof(MainPage),
new PropertyMetadata(new PropertyChangedCallback(OnScrollViewVerticalOffsetChanged))
);
public static readonly DependencyProperty ScrollViewHorizontalOffsetProperty =
DependencyProperty.Register(
"ScrollViewHorizontalOffset",
typeof(double),
typeof(MainPage),
new PropertyMetadata(new PropertyChangedCallback(OnScollViewHorizontalOffsetChanged))
);
private ScrollViewer _listScrollViewer;
private void ScrollViewer_Loaded_1(object sender, RoutedEventArgs e)
{
_listScrollViewer = sender as ScrollViewer;
Binding binding1 = new Binding();
binding1.Source = _listScrollViewer;
binding1.Path = new PropertyPath("VerticalOffset");
binding1.Mode = BindingMode.OneWay;
this.SetBinding(ScrollViewVerticalOffsetProperty, binding1);
Binding binding2 = new Binding();
binding2.Source = _listScrollViewer;
binding2.Path = new PropertyPath("HorizontalOffset");
binding2.Mode = BindingMode.OneWay;
this.SetBinding(ScrollViewHorizontalOffsetProperty, binding2);
}
public double ScrollViewVerticalOffset
{
get { return (double)this.GetValue(ScrollViewVerticalOffsetProperty); }
set { this.SetValue(ScrollViewVerticalOffsetProperty, value); }
}
public double ScrollViewHorizontalOffset
{
get { return (double)this.GetValue(ScrollViewHorizontalOffsetProperty); }
set { this.SetValue(ScrollViewHorizontalOffsetProperty, value); }
}
private static void OnScrollViewVerticalOffsetChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
MainPage page = obj as MainPage;
ScrollViewer viewer = page._listScrollViewer;
// ... do something here
}
private static void OnScollViewHorizontalOffsetChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
MainPage page = obj as MainPage;
ScrollViewer viewer = page._listScrollViewer;
// ... do something here
}
here's the XAML code I used
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0" LayoutUpdated='ContentPanel_LayoutUpdated'>
<ScrollViewer x:Name='scroller' VerticalAlignment='Stretch' VerticalScrollBarVisibility='Visible' >
<StackPanel
x:Name='listItems'></StackPanel>
</ScrollViewer>
</Grid>
and here's the C# code behind
private void ContentPanel_LayoutUpdated(object sender, EventArgs e)
{
var offset = scroller.VerticalOffset;
}
whenever the scroller is scrolled then the layout of the Grid (Container grid) changes so layout updated event is fired ... please try debugging by placing break point inside the event and look for the offset value ..
Add the property ManipulationMode="Control" to your ScrollViewer. This is needed because otherwise the UI thread will not be notified with enough ScrollViewer scroll values to get a fluid animation – the normal mode is a performance optimization from Windows Phone that you need to bypass!
I would like to know, how I could make the transition between two background colors more smooth. Is it possible to make some kind of fade transition?
I have created this little sample project to illustrate the behavior.
MainWindow.xaml
<Window x:Class="FadeTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Converter="clr-namespace:FadeTest" Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Converter:BackgroundPercentConverter x:Key="backgroundPercentConverter"/>
</Window.Resources>
<DockPanel>
<Label Content="{Binding PercentComplete}" Height="100" Width="200"
DockPanel.Dock="Top" Foreground="White" FontSize="28"
Background="{Binding PercentComplete, Converter={StaticResource backgroundPercentConverter}}"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center" />
<Button DockPanel.Dock="Bottom" Click="Button_Click" Width="100" Height="32">Click</Button>
</DockPanel>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MyClass {PercentComplete = 0};
}
private void Button_Click(object sender, RoutedEventArgs e)
{
((MyClass) DataContext).PercentComplete++;
}
}
MyClass.cs
class MyClass : INotifyPropertyChanged
{
private int _percentComplete;
public int PercentComplete
{
get { return _percentComplete; }
set
{
if (value >= 10)
value = 0;
_percentComplete = value;
OnPropertyChanged("PercentComplete");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
BackgroundPercentConverter.cs
public class BackgroundPercentConverter : IValueConverter
{
private static readonly SolidColorBrush[] MyColors = new[]
{
new SolidColorBrush(Color.FromRgb(229, 29, 37)),
new SolidColorBrush(Color.FromRgb(252, 52, 0)),
new SolidColorBrush(Color.FromRgb(253, 81, 0)),
new SolidColorBrush(Color.FromRgb(255, 101, 1)),
new SolidColorBrush(Color.FromRgb(255, 133, 0)),
new SolidColorBrush(Color.FromRgb(254, 175, 0)),
new SolidColorBrush(Color.FromRgb(221, 182, 3)),
new SolidColorBrush(Color.FromRgb(173, 216, 2)),
new SolidColorBrush(Color.FromRgb(138, 191, 62)),
new SolidColorBrush(Color.FromRgb(47, 154, 69))
};
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if ((int)value < 0 || (int)value >= MyColors.Length)
return MyColors[0];
return MyColors[(int)value];
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
You could define a StoryBoard which has a ColorAnimation which defines a from and to colour i.e. the colours you want to interpolate smoothly between.
Your animation should target the Color property of a SolidColorBrush which can either be one that is defined as a Resource and is then referenced by your control (i.e. your Label uses {StaticResource xxxxxx} in the Background to refer to it, or your animation could target the Background in your Label directly IF the Background contains a Brush which is a SolidColorBrush (...normally that is the case...but that depends entirely on how the Template for the control is designed).
Then you would need to begin and pause the animation and seek to the position in the animation that corresponds to your percentage. E.g. you could set the Duration to 100 seconds, and then you could simply use your percentage value as the number of seconds in a TimeSpan. (Might have to set IsControllable=true).
You would start and pause the animation when the control is loaded, and then change the seek position in sync with your percentage change.
Note, it's probably a bad idea just having an animation running all the time, just to map a range value to a presentation colour, but it's another option to what you have.
Otherwise stick to your valueconverter and you could calculate the linear interpolation between the 2 colours yourself. See this link:
Algorithm: How do I fade from Red to Green via Yellow using RGB values?
Here are some links related to the animation:
http://social.msdn.microsoft.com/Forums/en/wpf/thread/d2517850-81cc-4d22-be7d-cad522540ad2
How to seek a WPF Storyboard to a specific frame/time offset when not running
I have a simple XAML file, it contains a Label whose Foreground property contains a binding:
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Width="200" Height="100" >
<Label Content="Sampletext" Foreground="{Binding Path=Color}" />
</Grid>
When I load the template and apply a DataContext the Foreground still has the default value.
Is it possible to get bound foreground value without rendering the Grid?
// Load template
string templatePath = "/WpfApplication1;component/Template.xaml";
Grid grid = Application.LoadComponent(new Uri(templatePath, UriKind.Relative)) as Grid;
// Set dataContext
grid.DataContext = new { Color = Brushes.Green };
// Foregound still has default value
var foreground = ((Label)grid.Children[0]).Foreground;
Project can be downloaded here: http://dl.dropbox.com/u/21096596/WpfApplication1.zip
try
lblName.GetBindingExpression(Label.ForegroundProperty).UpdateTarget();
before
var foreground = ((Label)grid.Children[0]).Foreground;
There are automatic DataContext change notifications, the binding will update if the necessary conditions are met. One of them is that the control is loaded (IsLoaded == true) which is not the case in your code. The control will only load if you add it to the your UI somewhere.
Example test code:
private void Button_Click(object sender, RoutedEventArgs e)
{
Grid grid = null;
Action action = () =>
{
var foreground = ((Label)grid.Children[0]).Foreground;
MessageBox.Show(foreground.ToString());
grid.DataContext = new { Color = Brushes.Green };
foreground = ((Label)grid.Children[0]).Foreground;
MessageBox.Show(foreground.ToString());
};
grid = Application.LoadComponent(new Uri("Stuff/GridOne.xaml", UriKind.Relative)) as Grid;
if (grid.IsLoaded)
{
action();
}
else
{
grid.Loaded += (s, _) => action();
}
// This adds the grid to some StackPanel, if you do not do something like this
// nothing will happen since the control will not be loaded and thus the event
// will not fire, etc.
ControlStack.Children.Add(grid);
}
Why do you need the onetime binding? remove that, and it should work.
Wrap your DataContext in an object, and implement INotifyPropertyChanged, then the binding will update when the property changes, and there's no need to update the binding manually:
public class MyDataContext : INotifyPropertyChanged
{
private Brush color;
public Brush Color
{
get { return color; }
set
{
color = value;
RaisePropertyChanged("Color");
}
}
//implementation of PropertyChanged and RaisePropertyChanged omitted
}
and then update it like so:
var dc = new MyDataContext();
grid.DataContext = dc;
dc.Color = Brushes.Green; //this will trigger the NotifyPropertyChanged and update the binding
//color should be changed now
var foreground = ((Label)grid.Children[0]).Foreground;
Hopefully this helps...
If you want a property of a control to be binding to a property of DataContext but want to change the datacontext in runtime, there is a much more simple way to do it.
Create a ContentControl, then use ContentControl.ContentTemplate
<ContentControl Content=something>
<ContentControl.ContentTemplate>
<DataTemplate>
<Label Foreground="{Binding Path=Color}" />
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
Change the Content of the ContentControl instead of changing DataContext of the Label.