Animating textbox text bound to a double property on property value change - c#

I'd like to animate the transition from the old to the new value as seen in many software, i.e. when the value of the bound property changes, I'd like to increase or decrease the text of the text box by a specific offset until it reaches the new value. As an example:
Initial value: 26.0%
New value: 43.5%
Animation:
26.5% -> 30.0% -> 30.5% ....... -> 43.4
Is it possible to do this with the standard equipment of .Net or do you need a custom control?
Thanks in advance for any help.

I'm afraid I don't know if .NET has a custom control. But you can do it very easily with special supervision.
Step One : Create a timer
Step Two : Run timer1 object in form load event [timerName.Start();]
Step Three : Create a global variable to control the second. The local variable
Step Four : Check textbox contents by performing a second check on the tick event of the Timer object
private void Form1_Load(object sender, EventArgs e)
{
timer1.Start();
}
private int elapsed;
private void timer1_Tick(object sender, EventArgs e)
{
elapsed = elapsed + 1;
if (elapsed == 15)
{
textBox1.Text = "%" + "next value 1";
}
if (elapsed == 30)
{
textBox1.Text = "%" + "next value 2";
}
if (elapsed == 45)
{
textBox1.Text = "%" + "next value 3";
}
}
I hope I could help. If I can't help you, please write it down. Good coding!

An example of an auxiliary proxy for a discrete animation of a Double number:
using System;
using System.Windows;
namespace Proxy
{
public partial class DeltaNumberAnimator : Freezable
{
protected override Freezable CreateInstanceCore()
{
throw new NotImplementedException();
}
}
}
using System;
using System.Windows;
namespace Proxy
{
public partial class DeltaNumberAnimator
{
/// <summary>Source of number</summary>
public double Source
{
get => (double)GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
/// <summary><see cref="DependencyProperty"/> for property <see cref="Source"/>.</summary>
public static readonly DependencyProperty SourceProperty =
DependencyProperty.Register(nameof(Source), typeof(double), typeof(DeltaNumberAnimator), new PropertyMetadata(0d, (d, e) => ((DeltaNumberAnimator)d).SourceChanged(e)));
/// <summary>Number with animated change</summary>
public double Number
{
get => (double)GetValue(NumberProperty);
private set => SetValue(NumberPropertyKey, value);
}
private static readonly DependencyPropertyKey NumberPropertyKey =
DependencyProperty.RegisterReadOnly(nameof(Number), typeof(double), typeof(DeltaNumberAnimator), new PropertyMetadata(0d, NumberChanged));
/// <summary><see cref="DependencyProperty"/> for property <see cref="Number"/>.</summary>
public static readonly DependencyProperty NumberProperty = NumberPropertyKey.DependencyProperty;
/// <summary>Sets the delta value for a discrete change in the <see cref="Number"/> property.</summary>
public double Delta
{
get => (double)GetValue(DeltaProperty);
set => SetValue(DeltaProperty, value);
}
/// <summary><see cref="DependencyProperty"/> for property <see cref="Delta"/>.</summary>
public static readonly DependencyProperty DeltaProperty =
DependencyProperty.Register(nameof(Delta), typeof(double), typeof(DeltaNumberAnimator), new PropertyMetadata(0.01, DeltaChanged, CoerceDelta));
/// <summary>Number increment interval. </summary>
public TimeSpan Interval
{
get => (TimeSpan)GetValue(IntervalProperty);
set => SetValue(IntervalProperty, value);
}
/// <summary><see cref="DependencyProperty"/> for property <see cref="Interval"/>.</summary>
public static readonly DependencyProperty IntervalProperty =
DependencyProperty.Register(nameof(Interval), typeof(TimeSpan), typeof(DeltaNumberAnimator), new PropertyMetadata(TimeSpan.Zero, IntervalChanged));
/// <summary>Animation in progress.</summary>
public bool IsAnimation
{
get => (bool)GetValue(IsAnimationProperty);
private set => SetValue(IsAnimationPropertyKey, value);
}
public static readonly DependencyPropertyKey IsAnimationPropertyKey =
DependencyProperty.RegisterReadOnly(nameof(IsAnimation), typeof(bool), typeof(DeltaNumberAnimator), new PropertyMetadata(false));
/// <summary><see cref="DependencyProperty"/> for property <see cref="IsAnimation"/>.</summary>
public static readonly DependencyProperty IsAnimationProperty = IsAnimationPropertyKey.DependencyProperty;
}
}
using System;
using System.Windows;
using System.Windows.Threading;
namespace Proxy
{
public partial class DeltaNumberAnimator
{
private readonly DispatcherTimer timer = new DispatcherTimer();
private double source;
private double delta;
private double number;
public DeltaNumberAnimator()
{
timer.Tick += OnTick;
}
private void OnTick(object sender, EventArgs e)
{
if (NextStep())
{
timer.Stop();
IsAnimation = false;
}
}
private static void IntervalChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((DeltaNumberAnimator)d).timer.Interval = (TimeSpan)e.NewValue;
}
private void SourceChanged(DependencyPropertyChangedEventArgs e)
{
source = (double)e.NewValue;
if (source == number)
{
timer.Stop();
IsAnimation = false;
return;
}
if (timer.Interval == TimeSpan.Zero ||
delta == 0d)
{
Number = source;
timer.Stop();
IsAnimation = false;
return;
}
if (!NextStep())
{
timer.Start();
IsAnimation = true;
}
}
// Changing Number by Delta towards Source.
// Returns true if the Source value is reached.
private bool NextStep()
{
if (number < source)
{
double next = number + delta;
if (next >= source)
{
Number = source;
return true;
}
else
{
Number = next;
return false;
}
}
else
{
double next = number - delta;
if (next <= source)
{
Number = source;
return true;
}
else
{
Number = next;
return false;
}
}
}
private static object CoerceDelta(DependencyObject d, object baseValue)
{
double num = (double)baseValue;
if (num < double.Epsilon && num > -double.Epsilon)
return 0d;
if (num > 0)
return baseValue;
return -num;
}
private static void DeltaChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((DeltaNumberAnimator)d).delta = (double)e.NewValue;
}
private static void NumberChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((DeltaNumberAnimator)d).number = (double)e.NewValue;
}
}
}
Usage example:
namespace DeltaNumber
{
public class ViewModel
{
public double NumberProperty { get; set; }
}
}
<Window x:Class="DeltaNumber.DeltaNumberTestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DeltaNumber" xmlns:proxy="clr-namespace:Proxy;assembly=Common"
mc:Ignorable="d"
Title="DeltaNumberTestWindow" Height="450" Width="800">
<FrameworkElement.DataContext>
<local:ViewModel/>
</FrameworkElement.DataContext>
<UniformGrid Columns="1">
<FrameworkElement.Resources>
<proxy:DeltaNumberAnimator x:Key="deltaAnimator"
Source="{Binding NumberProperty}"
Delta="1"
Interval="0:0:1"/>
</FrameworkElement.Resources>
<TextBlock Text="{Binding Number, Source={StaticResource deltaAnimator}}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding IsAnimation, Source={StaticResource deltaAnimator}}"
Value="True">
<Setter Property="Background" Value="LightGreen"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBox Text="{Binding NumberProperty}"/>
<TextBox Text="Any text. Used only to lose focus in an binded TextBox."/>
</UniformGrid>
</Window>

Related

C# From random letters displaying to a text reveal effect for WPF/WinUI XAML TextBlock control

I'm trying to create a custom control in WinUI 3 based on a TextBlock control, that has to show the contained text using a random letter rolling effect (letter by letter) till the full text is revealed. Let's say something like the Airport Departures billboard...
To better understand the desired effect, this is a video where the desired result is made using Adobe After Effects.
How can I implement the (closest possible) effect in C# code?
In my solution, the animation is triggered by setting the Text DependencyProperty. That allows to easily restart the effect if the Text property is overridden during the current animation. But you could also trigger it by some public method. But keep in mind that the TextBlock control can get slow with high amounts of text and fast text changes. If you encounter performance issues you should rewrite this for a Win2D CanvasControl.
LetterRevealTextBlock.xaml:
<UserControl
x:Class="Test.WinUI3.LetterRevealTextBlock"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Test.WinUI3"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<TextBlock x:Name="LRTB" FontFamily="Consolas"/>
</UserControl>
LetterRevealTextBlock.xaml.cs:
namespace Test.WinUI3;
public sealed partial class LetterRevealTextBlock : UserControl
{
private static readonly DependencyProperty TextProperty = DependencyProperty.Register(
"Text", typeof(string), typeof(LetterRevealTextBlock), new PropertyMetadata("", (d, e) => ((LetterRevealTextBlock)d).TextChanged(d, e)));
public string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); }
private static readonly DependencyProperty ScrollIntervalProperty = DependencyProperty.Register(
"ScrollInterval", typeof(int), typeof(LetterRevealTextBlock), new PropertyMetadata(40, (d, e) => ((LetterRevealTextBlock)d).ScrollIntervalChanged(d, e)));
public int ScrollInterval { get => (int)GetValue(ScrollIntervalProperty); set => SetValue(ScrollIntervalProperty, value); }
private static readonly DependencyProperty RevealIntervalProperty = DependencyProperty.Register(
"RevealInterval", typeof(int), typeof(LetterRevealTextBlock), new PropertyMetadata(200, (d, e) => ((LetterRevealTextBlock)d).RevealIntervalChanged(d, e)));
public int RevealInterval { get => (int)GetValue(RevealIntervalProperty); set => SetValue(RevealIntervalProperty, value); }
const string Chars = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789";
private Random Random = new Random();
private char[] TextArray {get; set; }
private int CurrentChar = 0;
private DispatcherTimer RevealTimer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(200) };
private DispatcherTimer ScrollTimer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(40) };
private void ScrollIntervalChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ScrollTimer.Interval = TimeSpan.FromMilliseconds((int)e.NewValue);
}
private void RevealIntervalChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
RevealTimer.Interval = TimeSpan.FromMilliseconds((int)e.NewValue);
}
private void TextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TextArray = (e.NewValue as string).ToCharArray();
CurrentChar = 0;
ScrollTimer.Start();
RevealTimer.Start();
}
private void ScrollTimer_Tick(object sender, object e)
{
for (int i = 0; i < Text.Length; i++)
{
if (i <= CurrentChar)
{
TextArray[i] = Text[i];
}
else
{
TextArray[i] = Text[i] == ' ' ? ' ' : Chars[Random.Next(Chars.Length - 1)];
}
}
LRTB.Text = new string(TextArray);
if (CurrentChar >= Text.Length - 1)
{
CurrentChar = 0;
ScrollTimer.Stop();
RevealTimer.Stop();
return;
}
}
private void RevealTimer_Tick(object sender, object e)
{
CurrentChar++;
}
public LetterRevealTextBlock()
{
this.InitializeComponent();
ScrollTimer.Tick += ScrollTimer_Tick;
RevealTimer.Tick += RevealTimer_Tick;
}
}

WPF Countdown user control issue

I am trying to use the final revision of this control from Stackexchange here:
https://codereview.stackexchange.com/questions/197197/countdown-control-with-arc-animation
When I use the code it counts down just a single second and finishes. I am not sure what issue is.
Hopefully someone can help out - thanks.
The code I am using is this:
Arc.cs
public class Arc : Shape
{
public Point Center
{
get => (Point)GetValue(CenterProperty);
set => SetValue(CenterProperty, value);
}
public static readonly DependencyProperty CenterProperty =
DependencyProperty.Register(nameof(Center), typeof(Point), typeof(Arc),
new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.AffectsRender));
public double StartAngle
{
get => (double)GetValue(StartAngleProperty);
set => SetValue(StartAngleProperty, value);
}
public static readonly DependencyProperty StartAngleProperty =
DependencyProperty.Register(nameof(StartAngle), typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));
public double EndAngle
{
get => (double)GetValue(EndAngleProperty);
set => SetValue(EndAngleProperty, value);
}
public static readonly DependencyProperty EndAngleProperty =
DependencyProperty.Register(nameof(EndAngle), typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(90.0, FrameworkPropertyMetadataOptions.AffectsRender));
public double Radius
{
get => (double)GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);
}
public static readonly DependencyProperty RadiusProperty =
DependencyProperty.Register(nameof(Radius), typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender));
public bool SmallAngle
{
get => (bool)GetValue(SmallAngleProperty);
set => SetValue(SmallAngleProperty, value);
}
public static readonly DependencyProperty SmallAngleProperty =
DependencyProperty.Register(nameof(SmallAngle), typeof(bool), typeof(Arc),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));
static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));
protected override Geometry DefiningGeometry
{
get
{
double startAngleRadians = StartAngle * Math.PI / 180;
double endAngleRadians = EndAngle * Math.PI / 180;
double a0 = StartAngle < 0 ? startAngleRadians + 2 * Math.PI : startAngleRadians;
double a1 = EndAngle < 0 ? endAngleRadians + 2 * Math.PI : endAngleRadians;
if (a1 < a0)
a1 += Math.PI * 2;
SweepDirection d = SweepDirection.Counterclockwise;
bool large;
if (SmallAngle)
{
large = false;
d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;
}
else
large = (Math.Abs(a1 - a0) < Math.PI);
Point p0 = Center + new Vector(Math.Cos(a0), Math.Sin(a0)) * Radius;
Point p1 = Center + new Vector(Math.Cos(a1), Math.Sin(a1)) * Radius;
List<PathSegment> segments = new List<PathSegment>
{
new ArcSegment(p1, new Size(Radius, Radius), 0.0, large, d, true)
};
List<PathFigure> figures = new List<PathFigure>
{
new PathFigure(p0, segments, true)
{
IsClosed = false
}
};
return new PathGeometry(figures, FillRule.EvenOdd, null);
}
}
}
Countdown.xaml
<UserControl x:Class="WpfApp.Countdown"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="450" Loaded="Countdown_Loaded">
<Viewbox>
<Grid Width="100" Height="100">
<Border Background="#222" Margin="5" CornerRadius="50">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<Label Foreground="#fff" Content="{Binding SecondsRemaining}" FontSize="50" Margin="0, -10, 0, 0" />
<Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -15, 0, 0" />
</StackPanel>
</Border>
<uc:Arc
x:Name="Arc"
Center="50, 50"
StartAngle="-90"
EndAngle="-90"
Stroke="#45d3be"
StrokeThickness="5"
Radius="45" />
</Grid>
</Viewbox>
</UserControl>
Countdown.xaml.cs
public partial class Countdown : UserControl
{
public Duration Duration
{
get => (Duration)GetValue(DurationProperty);
set => SetValue(DurationProperty, value);
}
public static readonly DependencyProperty DurationProperty =
DependencyProperty.Register(nameof(Duration), typeof(Duration), typeof(Countdown), new PropertyMetadata(new Duration()));
public int SecondsRemaining
{
get => (int)GetValue(SecondsRemainingProperty);
set => SetValue(SecondsRemainingProperty, value);
}
public static readonly DependencyProperty SecondsRemainingProperty =
DependencyProperty.Register(nameof(SecondsRemaining), typeof(int), typeof(Countdown), new PropertyMetadata(0));
public event EventHandler Elapsed;
private readonly Storyboard _storyboard = new Storyboard();
public Countdown()
{
InitializeComponent();
DoubleAnimation animation = new DoubleAnimation(-90, 270, Duration);
Storyboard.SetTarget(animation, Arc);
Storyboard.SetTargetProperty(animation, new PropertyPath(nameof(Arc.EndAngle)));
_storyboard.Children.Add(animation);
DataContext = this;
}
private void Countdown_Loaded(object sender, EventArgs e)
{
if (IsVisible)
Start();
}
public void Start()
{
Stop();
_storyboard.CurrentTimeInvalidated += Storyboard_CurrentTimeInvalidated;
_storyboard.Completed += Storyboard_Completed;
_storyboard.Begin();
}
public void Stop()
{
_storyboard.CurrentTimeInvalidated -= Storyboard_CurrentTimeInvalidated;
_storyboard.Completed -= Storyboard_Completed;
_storyboard.Stop();
}
private void Storyboard_CurrentTimeInvalidated(object sender, EventArgs e)
{
ClockGroup cg = (ClockGroup)sender;
if (cg.CurrentTime == null) return;
TimeSpan elapsedTime = cg.CurrentTime.Value;
SecondsRemaining = (int)Math.Ceiling((Duration.TimeSpan - elapsedTime).TotalSeconds);
}
private void Storyboard_Completed(object sender, EventArgs e)
{
if (IsVisible)
Elapsed?.Invoke(this, EventArgs.Empty);
}
}
Your control is not properly initialized. You are currently not handling the property changes of the Duration property.
The dependency property values are applied after the control is instantiated (the constructor has returned): the XAML engine creates the element instance and then assigns the resources (e.g. a Style) and local values.
Therefore, your control will currently configure the animation (in the constructor) using the property's default Duration value (which is Duration.Automatic).
Generally, you must always assume that control properties are changing, e.g., via data binding or animation. To handle this scenarios you must register a dependency property changed callback - at least for every public property that has a direct impact on the behavior of the control.
SecondsRemaining should be a read-only dependency property.
You should use a TextBlock instead of a Label to display text.
To fix your issue, you must register a property changed callback for the Duration property to update the DoubleAnimation that depends on the value. Then store the actual DoubleAnimation in a private property, so that you can change its Duration on property changes:
public partial class Countdown : UserControl
{
public Duration Duration
{
get => (Duration)GetValue(DurationProperty);
set => SetValue(DurationProperty, value);
}
// Register the property changed callback
public static readonly DependencyProperty DurationProperty = DependencyProperty.Register(
nameof(Duration),
typeof(Duration),
typeof(Countdown),
new PropertyMetadata(new Duration(), OnDurationChanged));
// Store the DoubleAnimation in order to modify the Duration on property changes
private Timeline Timeline { get; set; }
public Countdown()
{
InitializeComponent();
// Store the DoubleAnimation in order to modify the Duration on property changes
this.Timeline = new DoubleAnimation(-90, 270, Duration);
Storyboard.SetTarget(this.Timeline, this.Arc);
Storyboard.SetTargetProperty(this.Timeline, new PropertyPath(nameof(Arc.EndAngle)));
_storyboard.Children.Add(this.Timeline);
DataContext = this;
}
// Handle the Duration property changes
private static void OnDurationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var this_ = d as Countdown;
this_.Timeline.Duration = (Duration)e.NewValue;
}
}

WPF Notify content change when it's property changed

I've set label Content to some custom class:
<Label>
<local:SomeContent x:Name="SomeContent" some="abc" />
</Label>
This properly displays "abc" in a view. However I can't figure out how do I notify the Label that the content property have changed i.e. this:
SomeContent.some = "xyz";
Will not cause the label to update it's view.
I know I can set binding to label's Content property. I have already like 7 different, working methods to achieve automatic update. However I'm interested in this particular behavior because it will save me a ton of work in some scenarios i.e the requirements are:
Label content is always the same SomeContent instance, only it's properties are changed.
No label content binding. The label should take a content object and refresh whenever the content is modified.
Initial value of some property can be set in XAML
some property can be changed in code, causing label refresh.
Am I missing something, or it's not possible?
This is my current implementation of SomeContent:
public class SomeContent : DependencyObject, INotifyPropertyChanged {
public static readonly DependencyProperty someProperty =
DependencyProperty.Register(nameof(some), typeof(string),
typeof(SomeContent),
new PropertyMetadata("", onDPChange)
);
private static void onDPChange(DependencyObject d, DependencyPropertyChangedEventArgs e) {
//throw new NotImplementedException();
(d as SomeContent).some = e.NewValue as String;
}
public event PropertyChangedEventHandler PropertyChanged;
public string some {
get => (string)GetValue(someProperty);
set {
SetValue(someProperty, value);
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(some))
);
}
}
public override string ToString() => some;
}
I turns out it's not possible to do it without third side code. So I wrote a helper class to do it easy now.
Dynamic object
public class SomeContent : IChangeNotifer {
public event Action<object> MODIFIED;
private string _some;
public string some {
get => _some;
set {
_some = value;
MODIFIED?.Invoke(this);
}
}
public override string ToString() => some;
}
You can add it to xaml file and it will be updated automatically. Single additional step is to add UIReseter somewhere bellow the elements that suppose to be auto-updated but that is needed only one for multiple contents in a tree.
Usage
<Window x:Class="DependencyContentTest.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:local="clr-namespace:DependencyContentTest"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<StackPanel>
<local:UIReseter />
<Label>
<local:SomeContent x:Name="SomeContent" some="abcd" />
</Label>
<Grid>
<Label>
<local:SomeContent x:Name="nested" some="nyest"/>
</Label>
</Grid>
</StackPanel>
</Window>
MainWindow code
public partial class MainWindow : Window {
private Timer t;
public MainWindow() {
InitializeComponent();
t = new Timer(onTimer, null, 5000, Timeout.Infinite);
MouseDown += (s,e) => { SomeContent.some = "iii"; };
}
private void onTimer(object state) {
Dispatcher.Invoke(() => {
SomeContent.some = "aaaa";
nested.some = "xxx";
});
}
}
And this is the helper class that handles the update
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using H = System.Windows.LogicalTreeHelper;
using FE = System.Windows.FrameworkElement;
using DO = System.Windows.DependencyObject;
using System.Reflection;
using System.Windows.Markup;
namespace DependencyContentTest
{
public interface IChangeNotifer {
/// <summary>Dispatched when this object was modified.</summary>
event Action<object> MODIFIED;
}
/// <summary>This element tracks nested <see cref="IChangeNotifer"/> descendant objects (in logical tree) of this object's parent element and resets a child in it's panel property.
/// Only static (XAML) objects are supported i.e. object added to the tree dynamically at runtime will not be tracked.</summary>
public class UIReseter : UIElement {
public int searchDepth { get; set; } = int.MaxValue;
protected override void OnVisualParentChanged(DO oldParent){
if (VisualParent is FE p) p.Loaded += (s, e) => bind(p);
}
private void bind(FE parent, int dl = 0) {
if (parent == null || dl > searchDepth) return;
var chs = H.GetChildren(parent);
foreach (object ch in chs) {
if (ch is UIReseter r && r != this) throw new Exception($#"There's overlapping ""{nameof(UIReseter)}"" instance in the tree. Use single global instance of check ""{nameof(UIReseter.searchDepth)}"" levels.");
if (ch is IChangeNotifer sc) trackObject(sc, parent);
else bind(ch as FE, ++dl);
}
}
private Dictionary<IChangeNotifer, Reseter> tracked = new Dictionary<IChangeNotifer, Reseter>();
private void trackObject(IChangeNotifer sc, FE parent) {
var cp = getContentProperty(parent);
if (cp == null) return;
var r = tracked.nev(sc, () => new Reseter {
child = sc,
parent = parent,
content = cp,
});
r.track();
}
private PropertyInfo getContentProperty(FE parent) {
var pt = parent.GetType();
var cp = parent.GetType().GetProperties(
BindingFlags.Public |
BindingFlags.Instance
).FirstOrDefault(i => Attribute.IsDefined(i,
typeof(ContentPropertyAttribute)));
return cp ?? pt.GetProperty("Content");
}
private class Reseter {
public DO parent;
public IChangeNotifer child;
public PropertyInfo content;
private bool isTracking = false;
/// <summary>Function called by <see cref="IChangeNotifer"/> on <see cref="IChangeNotifer.MODIFIED"/> event.</summary>
/// <param name="ch"></param>
public void reset(object ch) {
if(! isChildOf(child, parent)) return;
//TODO: Handle multi-child parents
content.SetValue(parent, null);
content.SetValue(parent, child);
}
public void track() {
if (isTracking) return;
child.MODIFIED += reset;
}
private bool isChildOf(IChangeNotifer ch, DO p) {
if(ch is DO dch) {
if (H.GetParent(dch) == p) return true;
child.MODIFIED -= reset; isTracking = false;
return false;
}
var chs = H.GetChildren(p);
foreach (var c in chs) if (c == ch) return true;
child.MODIFIED -= reset; isTracking = false;
return false;
}
}
}
public static class DictionaryExtension {
public static V nev<K,V>(this Dictionary<K,V> d, K k, Func<V> c) {
if (d.ContainsKey(k)) return d[k];
var v = c(); d.Add(k, v); return v;
}
}
}
It could be improved and it not fully tested but it works for current purposes.
Additional problem is that some elements like TextBox cry about not suppopring SomeContent, like it is so hard to use ToString()... but that is another story, and is not related to my question.
Updated answer:
I'd throw away implementing SomeContent as a Dependency property and use a UserControl instead:
<UserControl x:Class="WpfApp1.SomeContent"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<TextBlock Text="{Binding some, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SomeContent}}}"/>
</Grid>
</UserControl>
Then in code behind:
/// <summary>
/// Interaction logic for SomeContent.xaml
/// </summary>
public partial class SomeContent : UserControl
{
public static readonly DependencyProperty someProperty =
DependencyProperty.Register(nameof(some), typeof(string),
typeof(SomeContent),
new PropertyMetadata("")
);
public string some
{
get => (string)GetValue(someProperty);
set => SetValue(someProperty, value);
}
public SomeContent()
{
InitializeComponent();
}
}
Next, implement a view model that implements INotifyPropertyChanged:
public class MyViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _somePropertyOnMyViewModel;
public string SomePropertyOnMyViewModel
{
get => _somePropertyOnMyViewModel;
set { _somePropertyOnMyViewModel = value; OnPropertyChanged(); }
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Then create an instance of MyViewModel in your view and assign it to your view's DataContext:
public class MyView : Window
{
public MyView()
{
InitializeComponent();
DataContext = new MyViewModel();
}
}
Then, finally, in MyView use the markup I provided in my original answer:
<Label>
<local:SomeContent x:Name="SomeContent" some="{Binding
SomePropertyOnMyViewModel" />
</Label>

DependencyProperty not being attached

SetFocusIndex is called when navigating away from a view. This should register/attach a DependencyProperty to the specified control.
GetFocusIndex is called upon returning to the view. This should extract the registered/attached DependencyProperty which holds the index of the last control (EightTileGrid item etc) that had focus.
I see the correct DependencyProperty being set , but when I retrieve it on back navigation it returns -1 value as if the property was never set.
Setting logic:
public static readonly DependencyProperty FocusIndexProperty =
DependencyProperty.RegisterAttached(
"FocusIndex",
typeof(int),
typeof(GamePadFocusManager),
new PropertyMetadata(-1));
public static int GetFocusIndex(DependencyObject obj)
{
return (int)obj.GetValue(FocusIndexProperty);
}
public static void SetFocusIndex(DependencyObject obj, int value)
{
obj.SetValue(FocusIndexProperty, value);
}
private void OnNavigatingMessage(NavigatingMessage navigatingMessage)
{
Messenger.Default.Unregister<NavigatingMessage>(this, OnNavigatingMessage);
SaveFocusIndex();
}
private void SaveFocusIndex()
{
var controls = VisualTreeQueryHelper.FindChildrenOfType<Control>(this).ToList();
for (int i = 0; i < controls.Count; i++)
{
if (controls[i].ContainsFocus())
{
GamePadFocusManager.SetFocusIndex(this, i);
break;
}
}
}
Retrieving logic:
private void BaseTileGrid_GotFocus(object sender, RoutedEventArgs e)
{
if ((FocusManager.GetFocusedElement() == this) || !this.ContainsFocus())
{
SetFocusOnChildControl();
}
}
public virtual void SetFocusOnChildControl()
{
var focusIndex = Math.Max(0, GamePadFocusManager.GetFocusIndex(this));
var contentControls = VisualTreeQueryHelper.FindChildrenOfType<ContentControl>(this).ToList();
DispatcherHelper.BeginInvokeAtEndOfUiQueue(() =>
{
if (contentControls.Count > focusIndex)
{
if (this.ContainsFocus())
{
GamePadFocusManager.FocusOn(contentControls[focusIndex]);
}
}
});
}
Grid
public class BaseTileGrid : Control
{
...
}
Xaml:
<GridLayout:BaseTileGrid x:Name="recentlyWatched"
Style="{StaticResource FourByTwoGrid}"
ItemsSource="{Binding ItemViewModels}"
ItemTemplate="{StaticResource GridLayoutDataTemplateSelector}"
OverflowPageTitle="{Binding OverflowPageTitle}"
MoreButtonCommand="{Binding MoreButtonCommand}"/>

Applying MahApps.Metro style to NavigationWindow

Has anyone had any luck applying the MahApps.Metro style to a NavigationWindow? I have implemented it for a Window just fine, but need to apply it to a NavigationWindow with Pages. I tried extending NavigationWindow and adding the modifications from MetroWindow like so, but no luck. The Window has a standard title bar and border, and the content is completely black.
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Navigation;
using MahApps.Metro.Native;
namespace MahApps.Metro.Controls
{
[TemplatePart(Name = PART_TitleBar, Type = typeof(UIElement))]
[TemplatePart(Name = PART_WindowCommands, Type = typeof(WindowCommands))]
public class MetroNavigationWindow : NavigationWindow
{
private const string PART_TitleBar = "PART_TitleBar";
private const string PART_WindowCommands = "PART_WindowCommands";
public static readonly DependencyProperty ShowIconOnTitleBarProperty = DependencyProperty.Register("ShowIconOnTitleBar", typeof(bool), typeof(MetroNavigationWindow), new PropertyMetadata(true));
public static readonly DependencyProperty ShowTitleBarProperty = DependencyProperty.Register("ShowTitleBar", typeof(bool), typeof(MetroNavigationWindow), new PropertyMetadata(true));
public static readonly DependencyProperty ShowMinButtonProperty = DependencyProperty.Register("ShowMinButton", typeof(bool), typeof(MetroNavigationWindow), new PropertyMetadata(true));
public static readonly DependencyProperty ShowCloseButtonProperty = DependencyProperty.Register("ShowCloseButton", typeof(bool), typeof(MetroNavigationWindow), new PropertyMetadata(true));
public static readonly DependencyProperty ShowMaxRestoreButtonProperty = DependencyProperty.Register("ShowMaxRestoreButton", typeof(bool), typeof(MetroNavigationWindow), new PropertyMetadata(true));
public static readonly DependencyProperty TitlebarHeightProperty = DependencyProperty.Register("TitlebarHeight", typeof(int), typeof(MetroNavigationWindow), new PropertyMetadata(30));
public static readonly DependencyProperty TitleCapsProperty = DependencyProperty.Register("TitleCaps", typeof(bool), typeof(MetroNavigationWindow), new PropertyMetadata(true));
public static readonly DependencyProperty SaveWindowPositionProperty = DependencyProperty.Register("SaveWindowPosition", typeof(bool), typeof(MetroNavigationWindow), new PropertyMetadata(false));
public static readonly DependencyProperty WindowPlacementSettingsProperty = DependencyProperty.Register("WindowPlacementSettings", typeof(IWindowPlacementSettings), typeof(MetroNavigationWindow), new PropertyMetadata(null));
public static readonly DependencyProperty TitleForegroundProperty = DependencyProperty.Register("TitleForeground", typeof(Brush), typeof(MetroNavigationWindow));
public static readonly DependencyProperty IgnoreTaskbarOnMaximizeProperty = DependencyProperty.Register("IgnoreTaskbarOnMaximize", typeof(bool), typeof(MetroNavigationWindow), new PropertyMetadata(false));
public static readonly DependencyProperty GlowBrushProperty = DependencyProperty.Register("GlowBrush", typeof(SolidColorBrush), typeof(MetroNavigationWindow), new PropertyMetadata(null));
public static readonly DependencyProperty FlyoutsProperty = DependencyProperty.Register("Flyouts", typeof(FlyoutsControl), typeof(MetroNavigationWindow), new PropertyMetadata(null));
public static readonly DependencyProperty WindowTransitionsEnabledProperty = DependencyProperty.Register("WindowTransitionsEnabled", typeof(bool), typeof(MetroNavigationWindow), new PropertyMetadata(true));
bool isDragging;
public bool WindowTransitionsEnabled
{
get { return (bool)this.GetValue(WindowTransitionsEnabledProperty); }
set { SetValue(WindowTransitionsEnabledProperty, value); }
}
public FlyoutsControl Flyouts
{
get { return (FlyoutsControl)GetValue(FlyoutsProperty); }
set { SetValue(FlyoutsProperty, value); }
}
public WindowCommands WindowCommands { get; set; }
public bool IgnoreTaskbarOnMaximize
{
get { return (bool)this.GetValue(IgnoreTaskbarOnMaximizeProperty); }
set { SetValue(IgnoreTaskbarOnMaximizeProperty, value); }
}
public Brush TitleForeground
{
get { return (Brush)GetValue(TitleForegroundProperty); }
set { SetValue(TitleForegroundProperty, value); }
}
public bool SaveWindowPosition
{
get { return (bool)GetValue(SaveWindowPositionProperty); }
set { SetValue(SaveWindowPositionProperty, value); }
}
public IWindowPlacementSettings WindowPlacementSettings
{
get { return (IWindowPlacementSettings)GetValue(WindowPlacementSettingsProperty); }
set { SetValue(WindowPlacementSettingsProperty, value); }
}
public bool ShowIconOnTitleBar
{
get { return (bool)GetValue(ShowIconOnTitleBarProperty); }
set { SetValue(ShowIconOnTitleBarProperty, value); }
}
public bool ShowTitleBar
{
get { return (bool)GetValue(ShowTitleBarProperty); }
set { SetValue(ShowTitleBarProperty, value); }
}
public bool ShowMinButton
{
get { return (bool)GetValue(ShowMinButtonProperty); }
set { SetValue(ShowMinButtonProperty, value); }
}
public bool ShowCloseButton
{
get { return (bool)GetValue(ShowCloseButtonProperty); }
set { SetValue(ShowCloseButtonProperty, value); }
}
public int TitlebarHeight
{
get { return (int)GetValue(TitlebarHeightProperty); }
set { SetValue(TitlebarHeightProperty, value); }
}
public bool ShowMaxRestoreButton
{
get { return (bool)GetValue(ShowMaxRestoreButtonProperty); }
set { SetValue(ShowMaxRestoreButtonProperty, value); }
}
public bool TitleCaps
{
get { return (bool)GetValue(TitleCapsProperty); }
set { SetValue(TitleCapsProperty, value); }
}
public SolidColorBrush GlowBrush
{
get { return (SolidColorBrush)GetValue(GlowBrushProperty); }
set { SetValue(GlowBrushProperty, value); }
}
public string WindowTitle
{
get { return TitleCaps ? Title.ToUpper() : Title; }
}
public MetroNavigationWindow()
{
Loaded += this.MetroWindow_Loaded;
}
private void MetroWindow_Loaded(object sender, RoutedEventArgs e)
{
VisualStateManager.GoToState(this, "AfterLoaded", true);
if (!ShowTitleBar)
{
//Disables the system menu for reasons other than clicking an invisible titlebar.
IntPtr handle = new WindowInteropHelper(this).Handle;
UnsafeNativeMethods.SetWindowLong(handle, UnsafeNativeMethods.GWL_STYLE, UnsafeNativeMethods.GetWindowLong(handle, UnsafeNativeMethods.GWL_STYLE) & ~UnsafeNativeMethods.WS_SYSMENU);
}
if (this.Flyouts == null)
{
this.Flyouts = new FlyoutsControl();
}
}
static MetroNavigationWindow()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MetroNavigationWindow), new FrameworkPropertyMetadata(typeof(MetroNavigationWindow)));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (WindowCommands == null)
WindowCommands = new WindowCommands();
var titleBar = GetTemplateChild(PART_TitleBar) as UIElement;
if (ShowTitleBar && titleBar != null)
{
titleBar.MouseDown += TitleBarMouseDown;
titleBar.MouseUp += TitleBarMouseUp;
titleBar.MouseMove += TitleBarMouseMove;
}
else
{
MouseDown += TitleBarMouseDown;
MouseUp += TitleBarMouseUp;
MouseMove += TitleBarMouseMove;
}
}
protected override void OnStateChanged(EventArgs e)
{
if (WindowCommands != null)
{
WindowCommands.RefreshMaximiseIconState();
}
base.OnStateChanged(e);
}
protected void TitleBarMouseDown(object sender, MouseButtonEventArgs e)
{
var mousePosition = e.GetPosition(this);
bool isIconClick = ShowIconOnTitleBar && mousePosition.X <= TitlebarHeight && mousePosition.Y <= TitlebarHeight;
if (e.ChangedButton == MouseButton.Left)
{
if (isIconClick)
{
if (e.ClickCount == 2)
{
Close();
}
else
{
ShowSystemMenuPhysicalCoordinates(this, PointToScreen(new Point(0, TitlebarHeight)));
}
}
else
{
IntPtr windowHandle = new WindowInteropHelper(this).Handle;
UnsafeNativeMethods.ReleaseCapture();
var wpfPoint = PointToScreen(Mouse.GetPosition(this));
short x = Convert.ToInt16(wpfPoint.X);
short y = Convert.ToInt16(wpfPoint.Y);
int lParam = x | (y << 16);
UnsafeNativeMethods.SendMessage(windowHandle, Constants.WM_NCLBUTTONDOWN, Constants.HT_CAPTION, lParam);
if (e.ClickCount == 2 && (ResizeMode == ResizeMode.CanResizeWithGrip || ResizeMode == ResizeMode.CanResize))
{
WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
}
}
}
else if (e.ChangedButton == MouseButton.Right)
{
ShowSystemMenuPhysicalCoordinates(this, PointToScreen(mousePosition));
}
}
protected void TitleBarMouseUp(object sender, MouseButtonEventArgs e)
{
isDragging = false;
}
private void TitleBarMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed)
{
isDragging = false;
}
if (isDragging
&& WindowState == WindowState.Maximized
&& ResizeMode != ResizeMode.NoResize)
{
// Calculating correct left coordinate for multi-screen system.
Point mouseAbsolute = PointToScreen(Mouse.GetPosition(this));
double width = RestoreBounds.Width;
double left = mouseAbsolute.X - width / 2;
// Check if the mouse is at the top of the screen if TitleBar is not visible
if (!ShowTitleBar && mouseAbsolute.Y > TitlebarHeight)
return;
// Aligning window's position to fit the screen.
double virtualScreenWidth = SystemParameters.VirtualScreenWidth;
left = left + width > virtualScreenWidth ? virtualScreenWidth - width : left;
var mousePosition = e.MouseDevice.GetPosition(this);
// When dragging the window down at the very top of the border,
// move the window a bit upwards to avoid showing the resize handle as soon as the mouse button is released
Top = mousePosition.Y < 5 ? -5 : mouseAbsolute.Y - mousePosition.Y;
Left = left;
// Restore window to normal state.
WindowState = WindowState.Normal;
}
}
internal T GetPart<T>(string name) where T : DependencyObject
{
return (T)GetTemplateChild(name);
}
private static void ShowSystemMenuPhysicalCoordinates(Window window, Point physicalScreenLocation)
{
if (window == null) return;
var hwnd = new WindowInteropHelper(window).Handle;
if (hwnd == IntPtr.Zero || !UnsafeNativeMethods.IsWindow(hwnd))
return;
var hmenu = UnsafeNativeMethods.GetSystemMenu(hwnd, false);
var cmd = UnsafeNativeMethods.TrackPopupMenuEx(hmenu, Constants.TPM_LEFTBUTTON | Constants.TPM_RETURNCMD, (int)physicalScreenLocation.X, (int)physicalScreenLocation.Y, hwnd, IntPtr.Zero);
if (0 != cmd)
UnsafeNativeMethods.PostMessage(hwnd, Constants.SYSCOMMAND, new IntPtr(cmd), IntPtr.Zero);
}
}
}
To accomplish this, I am using a MetroWindow as my main navigation window, and a Frame within that MetroWindow which handles the navigation.
Navigation Window
<Controls:MetroWindow x:Class="TestWpfApplicationMahApps.Metro.NavWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro" Title="NavWindow" Height="300" Width="300">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Colours.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Accents/Blue.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Accents/BaseLight.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Frame Source="Page1.xaml" NavigationUIVisibility="Hidden"></Frame>
</Controls:MetroWindow>
This allows me to not change any of the navigation logic, it can be called from the NavigationService just like it was when using a NavigationWindow.
Page 1 .cs
/// <summary>
/// Interaction logic for Page1.xaml
/// </summary>
public partial class Page1 : Page
{
public Page1()
{
InitializeComponent();
}
private void button1_Click(object sender, RoutedEventArgs e)
{
NavigationService.Navigate(new Page2());
}
}

Categories