C# XAML Windows Universal Loading Dialog - c#

Is it possible to create a popup loading dialog in a Windows Universal app, which is similar to an Android progress dialog?
I have a section of my app that might take a bit of time to complete, so I'd like to show the user that the app is actually working. I want to do this by popping up a dialog overlay of some kind that shadows the background to stop user interaction.
Android Progress Dialog for comparison:

You can create a UserControl, with background color black and opacity arround 0.6
Then, put ProgressRing inside, create a DependencyProperty to control the activation of the ring from outside of the UserControl. Put the UserControl in XAML or create it at run time and inject into the Page.

UWP Progress Dialog
<UserControl
x:Class="HelperUWP.Controls.BusyIndicator"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:HelperUWP.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">
<UserControl.Resources>
<Style x:Key="LockScreenTextStyle" BasedOn="{StaticResource BaseTextBlockStyle}" TargetType="TextBlock">
<Setter Property="FontSize" Value="18"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="Margin" Value="15,0,0,0"/>
<Setter Property="TextAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</UserControl.Resources>
<Grid>
<Grid Background="Black" Opacity="0.4"/>
<Grid x:Name="gridBackground" HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="Transparent" Width="470" Height="120">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="1" Margin="15,5,15,5" Orientation="Vertical" HorizontalAlignment="Center">
<ProgressRing x:Name="progressRing" Height="40" Width="40" HorizontalAlignment="Center" IsActive="True"
/>
<TextBlock HorizontalAlignment="Center" Foreground="White" x:Name="TitleTextBlock"
Style="{StaticResource LockScreenTextStyle}" Text="Performing Operation"/>
</StackPanel>
</Grid>
</Grid>
//cs
public sealed partial class BusyIndicator : UserControl
{
private Popup ParentPopup = null;
public BusyIndicator(string title = "Loading...", SolidColorBrush backgroundColor = null, SolidColorBrush foregroundColor = null)
{
this.InitializeComponent();
TitleTextBlock.Text = title;
this.Text = title;
if (backgroundColor != null)
this.gridBackground.Background = backgroundColor;
if (foregroundColor != null)
{
this.progressRing.Foreground = foregroundColor;
this.TitleTextBlock.Foreground = foregroundColor;
}
}
static SolidColorBrush def = Application.Current.Resources["SystemControlBackgroundAccentBrush"] as SolidColorBrush;
#region Public Methods
/// <summary>
/// Closes the BusyIndicator.
/// </summary>
public void Close()
{
// Close the parent; closes the dialog too.
// ((Popup)Parent).IsOpen = false;
if(this.ParentPopup!= null && this.ParentPopup.IsOpen == true)
this.ParentPopup.IsOpen = false;
}
#endregion Public Methods
#region Public Static Methods
DispatcherTimer timer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(500) };
/// <summary>
/// Locks the screen ans starts the BusyIndicator by creating a popup.
/// </summary>
/// <param name="title">The title to be displayed by the BusyIndicator.</param>
/// <returns>The BusyIndicator.</returns>
public void Start()
{
// Create a popup with the size of the app's window.
Popup popup = new Popup()
{
Height = Window.Current.Bounds.Height,
IsLightDismissEnabled = false,
Width = Window.Current.Bounds.Width
};
// Create the BusyIndicator as a child, having the same size as the app.
BusyIndicator busyIndicator = new BusyIndicator()
{
Height = popup.Height,
Width = popup.Width,
Text = this.Text,
};
this.timer.Start();
this.timer.Tick += (f, y) =>
{
busyIndicator.Text = Text;
};
// Set the child of the popop
popup.Child = busyIndicator;
// Postion the popup to the upper left corner
popup.SetValue(Canvas.LeftProperty, 0);
popup.SetValue(Canvas.TopProperty, 0);
// Open it.
this.ParentPopup = popup;
popup.IsOpen = true;
// Return the BusyIndicator
// return (busyIndicator);
}
//string
public static readonly DependencyProperty SetMessageProperty = DependencyProperty.Register("Text", typeof(string),
typeof(BusyIndicator), new PropertyMetadata("Loading...", new PropertyChangedCallback(OnMessageTextChanged)));
public string Text
{
get { return (string)GetValue(SetMessageProperty); }
set { SetValue(SetMessageProperty, value); }
}
private static void OnMessageTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
BusyIndicator borderControl = d as BusyIndicator;
borderControl.OnMessageTextChanged(e);
}
private void OnMessageTextChanged(DependencyPropertyChangedEventArgs e)
{
this.TitleTextBlock.Text = e.NewValue.ToString();
}
//string
public static readonly DependencyProperty SetColorProperty = DependencyProperty.Register("SetColor", typeof(SolidColorBrush),
typeof(BusyIndicator), new PropertyMetadata(Colors.Green, new PropertyChangedCallback(OnColorTextChanged)));
public SolidColorBrush SetColor
{
get { return (SolidColorBrush)GetValue(SetColorProperty); }
set { SetValue(SetColorProperty, value); }
}
private static void OnColorTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
BusyIndicator borderControl = d as BusyIndicator;
borderControl.OnColorTextChanged(e);
}
private void OnColorTextChanged(DependencyPropertyChangedEventArgs e)
{
this.TitleTextBlock.Foreground =(SolidColorBrush) e.NewValue;
this.progressRing.Foreground = (SolidColorBrush)e.NewValue;
}
}
//
Result

Related

How to create a countdown bar animation in WPF(mvvm) with dynamic width and values bound to viewmodel?

I want to create an animation of a bar (rectangle) going from its current width to 0 which will be used as visualization of a countdown.
In the end it should look somehow like this:
Right now my trigger to start the animation is set in a static class (this part already works).
<Control x:Name="content" Grid.Column="0" Margin="0">
<Control.Template>
<ControlTemplate>
<Grid x:Name="FilledCountdownBar" Width="500" HorizontalAlignment="Left" >
<Rectangle Fill="#FFA4B5BF"/>
</Grid>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding Path=(Managers:ActionModeManager.ShowUiTimer)}" Value="true">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="FilledCountdownBar"
Storyboard.TargetProperty="(FrameworkElement.Width)"
To="0" Duration="0:1:0" AutoReverse="False"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Control.Template>
</Control>
I got several points which I do not get to work here:
This view will be located in the bottom of a window, which is scaleable. So I do not know the starting width in pixels at the beginning of the animation. I would love to remove the "Width" from the FilledCountdownBar element to let it fill the whole space at the start automatically, but then I cannot animate that value (getting an exception).
When I do not set the "From" property of the animation, then the animation does not reset because there is no start value and the width will remain 0 after the animation finished playing the first time.
I have a property (Duration type) in my viewmodel which I want to bind to the duration of the animation. But it looks like I cannot do that in control templates? An exception is thrown:
Cannot freeze this Storyboard timeline tree for use across threads.
I also tried to use a ProgressBar instead, but I could not get it animating smoothly. There were always small steps visible when changing the value, so like that it is not really an option for me.
Any help is welcome, thanks in advance.
When I need to have dynamic animations that rely on Widths and things like this, I always do them in code as attached behaviors or in custom control code.
This allows you to create a Storyboard in code, set all its dynamic properties and then start it.
In this case, once the animation kicks off, it will be for the size of the control once it starts. If the user resizes the window while it's running, the animation won't dynamically scale itself. However, you can indeed make that happen. I just implemented your simple DoubleAnimation.
Here is a working example for your case:
XAML
<Window x:Class="WpfApp4.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp4"
Title="MainWindow"
Width="800"
Height="450"
UseLayoutRounding="True">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Control x:Name="CountDownVisual"
Grid.Row="1"
Height="30"
Margin="0">
<Control.Template>
<ControlTemplate>
<Grid x:Name="RootElement">
<Grid x:Name="CountDownBarRootElement"
local:CountDownBarAnimationBehavior.IsActive="{Binding ShowUiTimer}"
local:CountDownBarAnimationBehavior.ParentElement="{Binding ElementName=RootElement}"
local:CountDownBarAnimationBehavior.TargetElement="{Binding ElementName=CountDownBar}">
<Rectangle x:Name="CountDownBar"
HorizontalAlignment="Left"
Fill="#FFA4B5BF" />
</Grid>
</Grid>
</ControlTemplate>
</Control.Template>
</Control>
</Grid>
</Window>
Attached Behavior
using System;
using System.Windows;
using System.Windows.Media.Animation;
using System.Windows.Threading;
namespace WpfApp4
{
public static class CountDownBarAnimationBehavior
{
private static Storyboard sb;
#region IsActive (DependencyProperty)
public static readonly DependencyProperty IsActiveProperty = DependencyProperty.RegisterAttached("IsActive", typeof(bool), typeof(CountDownBarAnimationBehavior), new FrameworkPropertyMetadata(false, OnIsActiveChanged));
public static bool GetIsActive(DependencyObject obj)
{
return (bool)obj.GetValue(IsActiveProperty);
}
public static void SetIsActive(DependencyObject obj, bool value)
{
obj.SetValue(IsActiveProperty, value);
}
private static void OnIsActiveChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is FrameworkElement control))
{
return;
}
if((bool)e.NewValue)
{
if (GetParentElement(control) != null)
{
StartAnimation(control);
}
else
{
// If IsActive is set to true and the other properties haven't
// been updated yet, defer the animation until render time.
control.Dispatcher?.BeginInvoke((Action) (() => { StartAnimation(control); }), DispatcherPriority.Render);
}
}
else
{
StopAnimation();
}
}
#endregion
#region ParentElement (DependencyProperty)
public static readonly DependencyProperty ParentElementProperty = DependencyProperty.RegisterAttached("ParentElement", typeof(FrameworkElement), typeof(CountDownBarAnimationBehavior), new FrameworkPropertyMetadata(null, OnParentElementChanged));
public static FrameworkElement GetParentElement(DependencyObject obj)
{
return (FrameworkElement)obj.GetValue(ParentElementProperty);
}
public static void SetParentElement(DependencyObject obj, FrameworkElement value)
{
obj.SetValue(ParentElementProperty, value);
}
private static void OnParentElementChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if(!(d is FrameworkElement fe))
{
return;
}
// You can wire up events here if you want to react to size changes, etc.
}
private static void OnParentElementSizeChanged(object sender, SizeChangedEventArgs e)
{
if (!(sender is FrameworkElement fe))
{
return;
}
if (GetIsActive(fe))
{
StopAnimation();
StartAnimation(fe);
}
}
#endregion
#region TargetElement (DependencyProperty)
public static readonly DependencyProperty TargetElementProperty = DependencyProperty.RegisterAttached("TargetElement", typeof(FrameworkElement), typeof(CountDownBarAnimationBehavior), new FrameworkPropertyMetadata(null));
public static FrameworkElement GetTargetElement(DependencyObject obj)
{
return (FrameworkElement)obj.GetValue(TargetElementProperty);
}
public static void SetTargetElement(DependencyObject obj, FrameworkElement value)
{
obj.SetValue(TargetElementProperty, value);
}
#endregion
private static void StartAnimation(DependencyObject d)
{
var parent = GetParentElement(d);
var target = GetTargetElement(d);
if (parent == null || target == null)
{
return;
}
sb = new Storyboard();
var da = new DoubleAnimation();
Storyboard.SetTarget(da, target);
Storyboard.SetTargetProperty(da, new PropertyPath("Width"));
da.AutoReverse = false;
da.Duration = new Duration(new TimeSpan(0, 1, 0));
da.From = parent.ActualWidth;
da.To = 0d;
sb.Children.Add(da);
sb.Begin();
}
private static void StopAnimation()
{
sb?.Stop();
}
}
}

Customer control's content actual size always return 0 when dynamically change content

In my control having content presenter which inside the canvas panel. I have arranged((Align as center, left and right in screen point) the content in canvas based on content actual size. Initial content size loaded properly and when dynamically change content, it actual size always return 0. Due to this, can't align content in screen position. Can you please advise me how to get content size in dynamic case like below scenario
<Page.Resources>
<!--control style-->
<Style TargetType="local:CustomLabel">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:CustomLabel">
<Grid>
<Canvas>
<Ellipse Height="10" Width="10" Canvas.Left="300" Canvas.Top="300" Fill="Red" />
</Canvas>
<Canvas>
<ContentPresenter Margin="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=LabelMargin}" Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content}" />
</Canvas>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Page.Resources>
<Grid>
<local:CustomLabel x:Name="label">
<local:CustomLabel.Content>
<Grid>
<TextBlock Text="Initial Label" />
</Grid>
</local:CustomLabel.Content>
</local:CustomLabel>
<Button Content="Change text" VerticalAlignment="Bottom" Height="75" Click="Button_Click" />
</Grid>
public MainPage()
{
this.InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
var grid = new Grid();
grid.Children.Add(new TextBlock() { Text = "Change" });
this.label.Content = grid;
}
// control implementation
public class CustomLabel : Control
{
bool loadTime = false;
public CustomLabel()
{
this.SizeChanged += CustomLabel_SizeChanged;
}
private void CustomLabel_SizeChanged(object sender, SizeChangedEventArgs e)
{
loadTime = true;
CalculateContentPosition();
}
private void CalculateContentPosition()
{
if (loadTime)
{
var content = Content as FrameworkElement;
if (content != null)
{
var left = content.ActualWidth / 2;
var top = content.ActualHeight / 2;
this.LabelMargin = new Thickness(300 - left, 300 - top, 0, 0);
}
}
}
public object Content
{
get { return (object)GetValue(ContentProperty); }
set { SetValue(ContentProperty, value); }
}
// Using a DependencyProperty as the backing store for Content. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ContentProperty =
DependencyProperty.Register("Content", typeof(object), typeof(CustomLabel), new PropertyMetadata(null,OnContentChanged));
private static void OnContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
(d as CustomLabel).CalculateContentPosition();
}
public Thickness LabelMargin
{
get { return (Thickness)GetValue(LabelMarginProperty); }
set { SetValue(LabelMarginProperty, value); }
}
// Using a DependencyProperty as the backing store for LabelMargin. This enables animation, styling, binding, etc...
public static readonly DependencyProperty LabelMarginProperty =
DependencyProperty.Register("LabelMargin", typeof(Thickness), typeof(CustomLabel), new PropertyMetadata(new Thickness(1)));
}
The issue was because you access its size too early, it even has not been rendered on XAML page. You might ask why the Grid has size on XAML page at first time. It's because the parent panel has done this operation(similar Measure(size)) when it layouts its child elements.
So, to solve your question, you could register the LayoutUpdated event of your 'CustomLabel'. Then, in its event handler, you could call CalculateContentPosition() method, instead of calling in OnContentChanged method.
public CustomLabel()
{
this.SizeChanged += CustomLabel_SizeChanged;
this.LayoutUpdated += CustomLabel_LayoutUpdated;
}
private void CustomLabel_LayoutUpdated(object sender, object e)
{
CalculateContentPosition();
}

WPF user control not updating path

I have a stripped down WPF example of a problem I am having in a much larger project. I have a user control called "UserControl1". The data context is set to self so I have dependency properties defined in the code-behind.
In the control I have an ItemsControl. In the ItemsSource I have a CompositeCollection that contains a CollectionContainer and a Line. The Line is there just to prove to myself that I am drawing.
I also have an object called "GraphPen" that contains a PathGeometry dependency property. The CollectionContainer of the user control contains an ObservableCollection of these GraphPens.
Now, I have a "MainWindow" to test the user control. In the MainWindow I have a DispatchTimer and in the Tick event of that timer, I add LineSegments to a PathFigure which has been added to the Figures collection of the PathGeometry of the single instance of the GraphPen.
I expect to see a diagonal line being drawn in parallel to the existing red line, but nothing shows up. If I put a break point at the end of the Tick event handler, I can examine the user control and drill down and see that the line segments do exist. For some reason they are not being rendered. I suspect I have done something wrong in the binding.
I will supply the code below.
GraphPen.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
namespace WpfExampleControlLibrary
{
public class GraphPen : DependencyObject
{
#region Constructor
public GraphPen()
{
PenGeometry = new PathGeometry();
}
#endregion Constructor
#region Dependency Properties
// Line Color
public static PropertyMetadata PenLineColorPropertyMetadata
= new PropertyMetadata(null);
public static DependencyProperty PenLineColorProperty
= DependencyProperty.Register(
"PenLineColor",
typeof(Brush),
typeof(GraphPen),
PenLineColorPropertyMetadata);
public Brush PenLineColor
{
get { return (Brush)GetValue(PenLineColorProperty); }
set { SetValue(PenLineColorProperty, value); }
}
// Line Thickness
public static PropertyMetadata PenLineThicknessPropertyMetadata
= new PropertyMetadata(null);
public static DependencyProperty PenLineThicknessProperty
= DependencyProperty.Register(
"PenLineThickness",
typeof(Int32),
typeof(GraphPen),
PenLineThicknessPropertyMetadata);
public Int32 PenLineThickness
{
get { return (Int32)GetValue(PenLineThicknessProperty); }
set { SetValue(PenLineThicknessProperty, value); }
}
// Pen Geometry
public static PropertyMetadata PenGeometryMetadata = new PropertyMetadata(null);
public static DependencyProperty PenGeometryProperty
= DependencyProperty.Register(
"PenGeometry",
typeof(PathGeometry),
typeof(UserControl1),
PenGeometryMetadata);
public PathGeometry PenGeometry
{
get { return (PathGeometry)GetValue(PenGeometryProperty); }
set { SetValue(PenGeometryProperty, value); }
}
#endregion Dependency Properties
}
}
UserControl1.xaml
<UserControl Name="ExampleControl"
x:Class="WpfExampleControlLibrary.UserControl1"
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:WpfExampleControlLibrary"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<DataTemplate DataType="{x:Type local:GraphPen}">
<Path Stroke="{Binding Path=PenLineColor}"
StrokeThickness="{Binding Path=PenLineThickness}"
Data="{Binding Path=Geometry}">
</Path>
</DataTemplate>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="40"/>
</Grid.RowDefinitions>
<ItemsControl Grid.Column="0" Grid.Row="0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="Aquamarine">
<Canvas.LayoutTransform>
<ScaleTransform ScaleX="1" ScaleY="-1" CenterX=".5" CenterY=".5"/>
</Canvas.LayoutTransform>
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemsSource>
<CompositeCollection>
<CollectionContainer
Collection="{
Binding Source={RelativeSource Self},
Path=GraphPens,
Mode=OneWay}"/>
<Line X1="10" Y1="0" X2="200" Y2="180" Stroke="DarkRed" StrokeThickness="2"/>
</CompositeCollection>
</ItemsControl.ItemsSource>
</ItemsControl>
<TextBox x:Name="debug" Grid.Column="0" Grid.Row="1" Text="{Binding Path=DebugText}"/>
</Grid>
</UserControl>
UserControl1.xaml.cs
namespace WpfExampleControlLibrary
{
/// <summary>
/// Interaction logic for UserControl1.xaml
/// </summary>
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
GraphPens = new ObservableCollection<GraphPen>();
}
#region Dependency Properties
// Pens
public static PropertyMetadata GraphPenMetadata = new PropertyMetadata(null);
public static DependencyProperty GraphPensProperty
= DependencyProperty.Register(
"GraphPens",
typeof(ObservableCollection<GraphPen>),
typeof(UserControl1),
GraphPenMetadata);
public ObservableCollection<GraphPen> GraphPens
{
get { return (ObservableCollection<GraphPen>)GetValue(GraphPensProperty); }
set { SetValue(GraphPensProperty, value); }
}
// Debug Text
public static PropertyMetadata DebugTextMetadata = new PropertyMetadata(null);
public static DependencyProperty DebugTextProperty
= DependencyProperty.Register(
"DebugText",
typeof(string),
typeof(UserControl1),
DebugTextMetadata);
public string DebugText
{
get { return (string)GetValue(DebugTextProperty); }
set { SetValue(DebugTextProperty, value); }
}
#endregion Dependency Properties
}
}
MainWindow.xaml
<Window x:Class="POC_WPF_UserControlExample.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:Exmpl="clr-namespace:WpfExampleControlLibrary;assembly=WpfExampleControlLibrary"
xmlns:local="clr-namespace:POC_WPF_UserControlExample"
mc:Ignorable="d"
Title="MainWindow" Height="550" Width="550">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition />
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="100" />
<RowDefinition />
<RowDefinition Height="100" />
</Grid.RowDefinitions>
<Exmpl:UserControl1 Grid.Column="1" Grid.Row="1" x:Name="myExample"/>
</Grid>
</Window>
MainWindow.xaml.cs
namespace POC_WPF_UserControlExample
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private DispatcherTimer _timer = null;
private GraphPen _graphPen0 = null;
private Int32 _pos = 0;
private PathFigure _pathFigure = null;
public MainWindow()
{
InitializeComponent();
_graphPen0 = new GraphPen();
_graphPen0.PenLineColor = Brushes.DarkGoldenrod;
_graphPen0.PenLineThickness = 2;
myExample.GraphPens.Add(_graphPen0);
_timer = new DispatcherTimer();
_timer.Tick += Timer_Tick;
_timer.Interval = new TimeSpan(0, 0, 0, 0, 100);
_timer.Start();
}
private void Timer_Tick(object sender, EventArgs e)
{
_pos++;
Point penPoint0 = new Point(_pos, _pos + 20);
if (_graphPen0.PenGeometry.Figures.Count == 0)
{
_pathFigure = new PathFigure();
_graphPen0.PenGeometry.Figures.Add(_pathFigure);
_pathFigure.StartPoint = penPoint0;
}
else
{
LineSegment segment = new LineSegment(penPoint0, false);
_pathFigure.Segments.Add(segment);
}
myExample.DebugText = _pos.ToString();
}
}
}
Screen Shot
I did not need INotifyPropertyChanged, and I did not need to recreate PenGeometry. I'm sorry I wasted your time with those ideas.
I've got your code drawing... something. A line that grows. I don't know if it's drawing exactly what you want, but you can figure out that part now that you can see what it is drawing.
First, minor copy/paste error in GraphPen.cs:
public static DependencyProperty PenGeometryProperty
= DependencyProperty.Register(
"PenGeometry",
typeof(PathGeometry),
typeof(UserControl1),
PenGeometryMetadata);
Owner type parameter needs to be GraphPen, not UserControl1:
typeof(PathGeometry),
typeof(GraphPen),
PenGeometryMetadata);
Second: Binding in UserControl1. Your binding to Self isn't going to work because Self, in that case, is the CollectionContainer you're binding on. Usually you'd use a source of RelativeSource={RelativeSource AncestorType=UserControl}, but CollectionContainer is not in the visual tree so that doesn't work (real intuitive, huh?). Instead we use a BindingProxy (source to follow):
<UserControl.Resources>
<!-- ... stuff ... -->
<local:BindingProxy
x:Key="UserControlBindingProxy"
Data="{Binding RelativeSource={RelativeSource AncestorType=UserControl}}"
/>
</UserControl.Resources>
And for the collection container...
<CollectionContainer
Collection="{
Binding
Source={StaticResource UserControlBindingProxy},
Path=Data.GraphPens,
Mode=OneWay}"
/>
Notice we're binding to Data.GraphPens; Data is the target of the proxy.
Also, we need an ItemTemplate for the ItemsControl, because it doesn't know how to display a GraphPen:
<ItemsControl.ItemTemplate>
<DataTemplate DataType="local:GraphPen">
<Border >
<Path
Data="{Binding PenGeometry}"
StrokeThickness="{Binding PenLineThickness}"
Stroke="{Binding PenLineColor, PresentationTraceSources.TraceLevel=None}"
/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
Note PresentationTraceSources.TraceLevel=None. Change None to High and it'll give you a lot of debugging info about the Binding in the VS Output pane. I added that when I was trying to figure out why I was setting PenLineColor to Brushes.Black in the constructor, but it kept coming out DarkGoldenrod at runtime. Can you say duhhh? Duhhh!
Now the binding proxy. What that does is you take any random object you want to use as a DataContext, bind it to Data on a BindingProxy instance defined as a resource, and you've got that DataContext available via a resource that you can get to with a StaticResource. If you're someplace where you can't get to something via the visual tree with RelativeSource, it's an option that you can rely on.
BindingProxy.cs
using System.Windows;
namespace WpfExampleControlLibrary
{
public class BindingProxy : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}
}
And finally, in MainWindow, you need to pass true for isStroked on the LineSegment instances:
LineSegment segment = new LineSegment(penPoint0, true);
Otherwise they're not drawn.
So now it's back in your lap, in warm goldenrod and soothing aquamarine. Ave, imperator, and all that.
Edit by original author!
Thank you Ed for all your effort.
I can't believe I missed the UserControl1 -> GraphPen
The BindingProxy will be very handy
The TraceLevel will also be handy, I had not used that before
I also corrected the DebugText binding and now that works
I never even noticed that!
In the DataTemplate why did we need to wrap the Path in a Border?
We don't. Before I got the Path showing, I added that to have something in the template that was guaranteed to be visible . It had a green border originally. I removed those attributes, but forgot to remove the Border itself.
If you look in my Timer_Tick you will note that now all I have to update is adding new segments. Hopefully, this will help performance. Your opinion?
No idea. I would actually put that segment adding code in GraphPen as AddSegment(Point pt) and AddSegment(float x, float y) => AddSegment(new Point(x,y)); overloads. I have a great allergy to putting logic in event handlers. The most I'll do is toss an if or a try/catch around a non-handler method that does the real work. Then I'd write AddSegment(Point pt) both ways and benchmark one against the other.
I will add my code for completeness:
UserControl1.xaml
<UserControl Name="ExampleControl"
x:Class="WpfExampleControlLibrary.UserControl1"
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:WpfExampleControlLibrary"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<local:BindingProxy
x:Key="UserControlBindingProxy"
Data="{Binding RelativeSource={RelativeSource AncestorType=UserControl}}"/>
<DataTemplate DataType="{x:Type local:GraphPen}">
<Border>
<Path
Data="{Binding PenGeometry}"
StrokeThickness="{Binding PenLineThickness}"
Stroke="{Binding
PenLineColor,
PresentationTraceSources.TraceLevel=None}"
/>
</Border>
</DataTemplate>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="40"/>
</Grid.RowDefinitions>
<ItemsControl Grid.Column="0" Grid.Row="0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="Aquamarine">
<Canvas.LayoutTransform>
<ScaleTransform ScaleX="1" ScaleY="-1" CenterX=".5" CenterY=".5"/>
</Canvas.LayoutTransform>
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemsSource>
<CompositeCollection>
<CollectionContainer
Collection="{Binding
Source={StaticResource UserControlBindingProxy},
Path=Data.GraphPens,
Mode=OneWay}"/>
<Line X1="10" Y1="0" X2="200" Y2="180" Stroke="DarkRed" StrokeThickness="2"/>
</CompositeCollection>
</ItemsControl.ItemsSource>
</ItemsControl>
<TextBox
x:Name="debug"
Grid.Column="0" Grid.Row="1"
Text="{Binding
Source={StaticResource UserControlBindingProxy},
Path=Data.DebugText,
Mode=OneWay}"/>
</Grid>
</UserControl>
UserControl1.xaml.cs
namespace WpfExampleControlLibrary
{
/// <summary>
/// Interaction logic for UserControl1.xaml
/// </summary>
public partial class UserControl1 : UserControl
{
#region Constructor
public UserControl1()
{
InitializeComponent();
GraphPens = new ObservableCollection<GraphPen>();
}
#endregion Constructor
#region Public Methods
#endregion Public Methods
#region Dependency Properties
// Pens
public static PropertyMetadata GraphPenMetadata = new PropertyMetadata(null);
public static DependencyProperty GraphPensProperty
= DependencyProperty.Register(
"GraphPens",
typeof(ObservableCollection<GraphPen>),
typeof(UserControl1),
GraphPenMetadata);
public ObservableCollection<GraphPen> GraphPens
{
get { return (ObservableCollection<GraphPen>)GetValue(GraphPensProperty); }
set { SetValue(GraphPensProperty, value); }
}
// Debug Text
public static PropertyMetadata DebugTextMetadata = new PropertyMetadata(null);
public static DependencyProperty DebugTextProperty
= DependencyProperty.Register(
"DebugText",
typeof(string),
typeof(UserControl1),
DebugTextMetadata);
public string DebugText
{
get { return (string)GetValue(DebugTextProperty); }
set { SetValue(DebugTextProperty, value); }
}
#endregion Dependency Properties
}
}
GraphPen.cs
namespace WpfExampleControlLibrary
{
public class GraphPen : DependencyObject
{
#region Constructor
public GraphPen()
{
PenGeometry = new PathGeometry();
}
#endregion Constructor
#region Dependency Properties
// Line Color
public static PropertyMetadata PenLineColorPropertyMetadata
= new PropertyMetadata(null);
public static DependencyProperty PenLineColorProperty
= DependencyProperty.Register(
"PenLineColor",
typeof(Brush),
typeof(GraphPen),
PenLineColorPropertyMetadata);
public Brush PenLineColor
{
get { return (Brush)GetValue(PenLineColorProperty); }
set { SetValue(PenLineColorProperty, value); }
}
// Line Thickness
public static PropertyMetadata PenLineThicknessPropertyMetadata
= new PropertyMetadata(null);
public static DependencyProperty PenLineThicknessProperty
= DependencyProperty.Register(
"PenLineThickness",
typeof(Int32),
typeof(GraphPen),
PenLineThicknessPropertyMetadata);
public Int32 PenLineThickness
{
get { return (Int32)GetValue(PenLineThicknessProperty); }
set { SetValue(PenLineThicknessProperty, value); }
}
// Pen Geometry
public static PropertyMetadata PenGeometryMetadata = new PropertyMetadata(null);
public static DependencyProperty PenGeometryProperty
= DependencyProperty.Register(
"PenGeometry",
typeof(PathGeometry),
typeof(GraphPen),
PenGeometryMetadata);
public PathGeometry PenGeometry
{
get { return (PathGeometry)GetValue(PenGeometryProperty); }
set { SetValue(PenGeometryProperty, value); }
}
#endregion Dependency Properties
}
}
BindingProxy.cs
namespace WpfExampleControlLibrary
{
public class BindingProxy : Freezable
{
#region Override Freezable Abstract Parts
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion Override Freezable Abstract Parts
#region Dependency Properties
// Using a DependencyProperty as the backing store for Data.
// This enables animation, styling, binding, etc...
public static PropertyMetadata DataMetadata = new PropertyMetadata(null);
public static readonly DependencyProperty DataProperty
= DependencyProperty.Register(
"Data",
typeof(object),
typeof(BindingProxy),
DataMetadata);
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
#endregion Dependency Properties
}
}
MainWindow.xaml
<Window x:Class="POC_WPF_UserControlExample.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:Exmpl="clr-namespace:WpfExampleControlLibrary;assembly=WpfExampleControlLibrary"
xmlns:local="clr-namespace:POC_WPF_UserControlExample"
mc:Ignorable="d"
Title="MainWindow" Height="550" Width="550">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition />
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="100" />
<RowDefinition />
<RowDefinition Height="100" />
</Grid.RowDefinitions>
<Exmpl:UserControl1 Grid.Column="1" Grid.Row="1" x:Name="myExample"/>
</Grid>
</Window>
MainWindow.xaml.cs
namespace POC_WPF_UserControlExample
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private DispatcherTimer _timer = null;
private GraphPen _graphPen0 = null;
private Int32 _pos = 0;
private PathFigure _pathFigure0 = null;
private bool _firstTime = true;
public MainWindow()
{
InitializeComponent();
_pathFigure0 = new PathFigure();
_graphPen0 = new GraphPen();
_graphPen0.PenLineColor = Brushes.DarkGoldenrod;
_graphPen0.PenLineThickness = 2;
_graphPen0.PenGeometry = new PathGeometry();
_graphPen0.PenGeometry.Figures.Add(_pathFigure0);
myExample.GraphPens.Add(_graphPen0);
_timer = new DispatcherTimer();
_timer.Tick += Timer_Tick;
_timer.Interval = new TimeSpan(0, 0, 0, 0, 100);
_timer.Start();
}
private void Timer_Tick(object sender, EventArgs e)
{
_pos++;
Point penPoint0 = new Point(_pos, _pos + 20);
if (_firstTime)
{
myExample.GraphPens[0].PenGeometry.Figures[0].StartPoint = penPoint0;
_firstTime = false;
}
else
{
LineSegment segment = new LineSegment(penPoint0, true);
myExample.GraphPens[0].PenGeometry.Figures[0].Segments.Add(segment);
}
myExample.DebugText = _pos.ToString();
}
}
}

WPF show "dummy" control until focused/clicked

I have a list of heavy Controls, which I don't want to render before the user interacts with them (one at a time).
I want to show a placeholder for each Control until the placeholder is clicked (preferably focused) and then render the real Control.
What I've tried looks like this:
<ContentControl x:Name="theControl">
<TextBox x:Name="TextBlock" Text="Placeholder right here."/>
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding IsFocused, ElementName=TextBlock}" Value="True">
<Setter Property="Content" >
<Setter.Value>
<Grid x:Name="theGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="CodeColumn"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock>Heavy control part1</TextBlock>
<TextBlock Grid.Column="1">heavy control part2</TextBlock>
</Grid>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
Anyone knows a better approach or what I'm missing?
I don't know if this is a better solution, but you can create the heavy control in code and then remove/add children after a GotFocus event.
Add a GotFocus event to your TextBlock and put the TextBlock in a Grid
<Grid Name="myGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="100*"/>
</Grid.RowDefinitions>
<TextBox x:Name="TextBlock" Grid.Column="0" Grid.Row="0" Text="Placeholder right here." GotFocus="TextBlock_GotFocus" />
</Grid>
Then in the cs file
private void TextBlock_GotFocus(object sender, RoutedEventArgs e)
{
createNewControl();
}
private void createNewControl()
{
Grid myOtherGrid = new Grid();
RowDefinition newRow1 = new RowDefinition();
newRow1.Height = new GridLength(100.0);
RowDefinition newRow2 = new RowDefinition();
newRow2.Height = new GridLength(100.0);
ColumnDefinition newColumn1 = new ColumnDefinition();
newColumn1.Width = new GridLength(50.0);
ColumnDefinition newColumn2 = new ColumnDefinition();
newColumn2.Width = new GridLength(50.0);
myOtherGrid.RowDefinitions.Add(newRow1);
myOtherGrid.RowDefinitions.Add(newRow2);
myOtherGrid.ColumnDefinitions.Add(newColumn1);
myOtherGrid.ColumnDefinitions.Add(newColumn2);
TextBox myOtherTextBlock1 = new TextBox();
myOtherTextBlock1.Text = "new block 1";
TextBox myOtherTextBlock2 = new TextBox();
myOtherTextBlock2.Text = "new block 1";
myOtherGrid.Children.Add(myOtherTextBlock1);
Grid.SetRow(myOtherTextBlock1, 0);
Grid.SetColumn(myOtherTextBlock1, 0);
myOtherGrid.Children.Add(myOtherTextBlock2);
Grid.SetRow(myOtherTextBlock2, 1);
Grid.SetColumn(myOtherTextBlock2, 1);
myGrid.Children.Remove(TextBlock);
myGrid.Children.Add(myOtherGrid);
}
This is the general idea of what I managed to get working.
public partial class PlaceHolder : UserControl
{
private bool m_isReadOnly;
private object m_PlaceholdeContent;
private bool m_hasValue;
private object m_realContent;
public PlaceHolder()
{
InitializeComponent();
GotFocus += OnGotFocus;
}
private void OnGotFocus(object sender, RoutedEventArgs routedEventArgs)
{
GotFocus -= OnGotFocus;
if (!RealContentIsUsed)
{
RealContentIsUsed = true;
Content = RealContent;
}
}
private bool RealContentIsUsed { get; set; }
public object RealContent
{
get { return m_realContent; }
set
{
m_realContent = value;
if (IsReadOnly || HasValue)
{
Content = m_realContent;
}
}
}
public object PlaceholdeContent
{
get { return m_PlaceholdeContent; }
set
{
m_PlaceholdeContent = value;
if (!RealContentIsUsed)
{
Content = m_PlaceholdeContent;
}
}
}
public bool IsReadOnly
{
get { return m_isReadOnly; }
set
{
m_isReadOnly = value;
if (value && !RealContentIsUsed)
{
Content = RealContent;
RealContentIsUsed = true;
}
}
}
public bool HasValue
{
get { return m_hasValue; }
set
{
m_hasValue = value;
if (HasValue && RealContentIsUsed == false)
{
Content = RealContent;
RealContentIsUsed = true;
}
}
}
}

Button user control with Dependency property ShowImage display image path instead of Image

I'm trying to create a button user control which can display Image from xaml by adding property (ShowImage="ImagePath").
I've bound the user control Image source to the button's Content in the xaml file:
<UserControl x:Class="testUserControl.UserControls.TestDependencyShowImage"
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"
mc:Ignorable="d"
Height="auto" Width="auto">
<Grid>
<Button MinHeight="30" MinWidth="50">
<Button.Template>
<ControlTemplate TargetType="Button">
<Image Source="{TemplateBinding Content}"/>
</ControlTemplate>
</Button.Template>
</Button>
</Grid>
</UserControl>
And I've created a Dependency property which create BitmapImage and set it to the content(in the meantime hard coded path just to see if it can be done).
cs:
namespace testUserControl.UserControls
{
/// <summary>
/// Interaction logic for TestDependencyShowImage.xaml
/// </summary>
public partial class TestDependencyShowImage : UserControl
{
private static BitmapImage s_oImage = null;
private static string s_strSourceImage = null;
public static readonly DependencyProperty ShowImageDP = DependencyProperty.Register("ShowImage", typeof(string), typeof(TestDependencyShowImage), new PropertyMetadata(null, new PropertyChangedCallback(SetImage)));
public string ShowImage
{
get
{
return (string)GetValue(ShowImageDP);
}
set
{
SetValue(ShowImageDP, value);
this.Content = s_oImage;
//OnTargetPowerChanged(this, new DependencyPropertyChangedEventArgs(TargetPowerProperty, value, value)); // Old value irrelevant.
}
}
private static void SetImage(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
TestDependencyShowImage muc = (TestDependencyShowImage)obj;
s_strSourceImage = (string)args.NewValue;
if (s_strSourceImage != null)
{
s_oImage = new BitmapImage(new Uri(#"C:\Users\AmitL\Desktop\james-brown-010.jpg", UriKind.Absolute));
//BitmapImage l_oImage = new BitmapImage(new Uri(value));
}
}
public TestDependencyShowImage()
{
InitializeComponent();
this.Content = s_oImage;
}
}
}
You really don't need any of that code. Just let the Framework convert the string file path into the image for you. Try this code inside your UserControl:
<Image Source="{Binding ShowImage, RelativeSource={RelativeSource
AncestorType={x:Type YourXamlNamespacePrefix:TestDependencyShowImage}}}" />

Categories