I am trying to build a UserControl but I am having trouble getting my bindings to work. I know I am missing something, but I can't figure out what it is. I am not getting any BindingExpressions
XAML
<UserControl x:Class="WpfApplication3.NumericUpDown"
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:WpfApplication3">
<Grid>
<StackPanel>
<TextBox Text="{Binding NumericUpDownText, Mode=TwoWay, RelativeSource={RelativeSource AncestorType={x:Type local:NumericUpDown}}}" LostFocus="PART_NumericUpDown_LostFocus">
<TextBox.Resources>
<Style TargetType="TextBox">
<Style.Triggers>
<DataTrigger Binding="{Binding AnyNumericErrors, RelativeSource={RelativeSource AncestorType={x:Type local:NumericUpDown}, AncestorLevel=1}}" Value="false">
<Setter Property="Background" Value="Blue" />
</DataTrigger>
<DataTrigger Binding="{Binding AnyNumericErrors, RelativeSource={RelativeSource AncestorType={x:Type local:NumericUpDown}, AncestorLevel=1}}" Value="true">
<Setter Property="Background" Value="Red" />
<Setter Property="ToolTip" Value="There is an error" />
</DataTrigger>
</Style.Triggers>
<EventSetter Event="LostFocus" Handler="PART_NumericUpDown_LostFocus" />
</Style>
</TextBox.Resources>
<TextBox.Template>
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ScrollViewer x:Name="PART_ContentHost"
Grid.Column="0" />
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<RepeatButton x:Name="PART_IncreaseButton"
Click="PART_IncreaseButton_Click"
Content="UP" />
<RepeatButton x:Name="PART_DecreaseButton"
Grid.Row="1"
Click="PART_DecreaseButton_Click"
Content="Down"/>
</Grid>
</Grid>
</Border>
</ControlTemplate>
</TextBox.Template>
</TextBox>
</StackPanel>
</Grid>
C# Code Behind
namespace WpfApplication3
{
/// <summary>
/// Interaction logic for NumericUpDown.xaml
/// </summary>
public partial class NumericUpDown : UserControl
{
public NumericUpDown()
{
InitializeComponent();
//AnyNumericErrors = true;
}
private String _NumericUpDownText;
public String NumericUpDownText
{
get { return _NumericUpDownText; }
set
{
_NumericUpDownText = value;
NotifyPropertyChanged("NumericUpDownText");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private void PART_NumericUpDown_LostFocus(object sender, RoutedEventArgs e)
{
CheckForErrors();
}
public void CheckForErrors()
{
try
{
int value = Int32.Parse(NumericUpDownText);
AnyNumericErrors = false;
}
catch (FormatException)
{
Debug.WriteLine("error");
AnyNumericErrors = true;
}
}
private Boolean m_AnyNumericErrors;
public Boolean AnyNumericErrors
{
get
{
return m_AnyNumericErrors;
}
set
{
m_AnyNumericErrors = value;
NotifyPropertyChanged("AnyNumericErrors");
}
}
#region DP
public Int32 LowerBound
{
get;
set;
}
public static readonly DependencyProperty LowerBoundProperty = DependencyProperty.Register("LowerBound", typeof(Int32), typeof(NumericUpDown));
#endregion
private void PART_IncreaseButton_Click(object sender, RoutedEventArgs e)
{
try
{
Int32 value = Int32.Parse(NumericUpDownText);
value++;
NumericUpDownText = value.ToString();
}
catch (Exception)
{
AnyNumericErrors = true;
}
}
private void PART_DecreaseButton_Click(object sender, RoutedEventArgs e)
{
try
{
Int32 value = Int32.Parse(NumericUpDownText);
value--;
NumericUpDownText = value.ToString();
}
catch (Exception)
{
AnyNumericErrors = true;
}
}
}
}
EDIT
The primary issue is that the DataTriggers are not working... at inception, the textbox is blue, but never changes. And when I press on up/down to increment/decrement the values, the events are getting called, but the value isn't changing in the UI
You should use DependencyProperties like:
public bool AnyNumericErrors
{
get { return (bool)GetValue(AnyNumericErrorsProperty); }
set { SetValue(AnyNumericErrorsProperty, value); }
}
// Using a DependencyProperty as the backing store for AnyNumericErrors. This enables animation, styling, binding, etc...
public static readonly DependencyProperty AnyNumericErrorsProperty =
DependencyProperty.Register("AnyNumericErrors", typeof(bool), typeof(NumericUpDown), new UIPropertyMetadata(false));
If that isn't enough, then remove the level restriction on you ancestor search.
Related
Suppose I have a Window or UserControl with a boatload of named elements. I want to change all these elements' property values based on a single property of either my view model or a custom DP on the parent (really doesn't matter which, because I can easily bind the DP to the view model property).
Here is a barebones example:
<Window x:Class="TriggerFun.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:TriggerFun"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Rectangle x:Name="Rect1"
Grid.Column="0"
Width="200" Height="200"
Fill="Red"/>
<Rectangle x:Name="Rect2"
Grid.Column="1"
Width="200" Height="200"
Fill="Yellow"/>
<Button Grid.Row="1"
Grid.ColumnSpan="2"
Click="Button_Click">
Swap!
</Button>
</Grid>
using System;
using System.ComponentModel;
using System.Windows;
namespace TriggerFun
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public ViewModel ViewModel => this.DataContext as ViewModel;
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
this.ViewModel.AlternativeLayout = !this.ViewModel.AlternativeLayout;
}
}
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
bool _alternativeLayout;
public bool AlternativeLayout
{
get => _alternativeLayout;
set
{
_alternativeLayout = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AlternativeLayout)));
}
}
}
}
What I would like to do in this example is swap the columns of the red and yellow Rectangles when the user clicks the button. (And yes of course I know I can do this in code-behind. I want to do this in pure XAML).
What would make eminent sense to me is if I could do add this to Window:
<Window.Triggers>
<DataTrigger Binding="{Binding AlternativeLayout}" Value="True">
<Setter TargetName="Rect1" Property="Grid.Column" Value="1"/>
<Setter TargetName="Rect2" Property="Grid.Column" Value="0"/>
</DataTrigger>
</Window.Triggers>
Well that doesn't work because I get a runtime error, Triggers collection members must be of type EventTrigger. So all triggers that are children of anything other than a Style, DataTemplate, or ControlTemplate I guess have to be EventTriggers? Fine.
So then I try this:
<Window.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding AlternativeLayout}" Value="True">
<Setter TargetName="Rect1" Property="Grid.Column" Value="1"/>
<Setter TargetName="Rect2" Property="Grid.Column" Value="0"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Style>
That won't even compile: TargetName property cannot be set on a Style Setter.
I know I can use TargetName in DataTemplate or ControlTemplate Triggers, but when defining UserControl's and Windows of course you usually don't set the DataTemplate but rather just set the child content directly.
The only thing I can do that I know works is take each element that I want to be changed and give it its own inline style with triggers, with the final XAML looking incredibly ugly:
<Rectangle x:Name="Rect1"
Width="200" Height="200"
Fill="Red">
<Rectangle.Style>
<Style TargetType="Rectangle">
<Setter Property="Grid.Column" Value="0"/>
<Style.Triggers>
<DataTrigger Binding="{Binding AlternativeLayout}" Value="True">
<Setter Property="Grid.Column" Value="1"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
<Rectangle x:Name="Rect2"
Width="200" Height="200"
Fill="Yellow">
<Rectangle.Style>
<Style TargetType="Rectangle">
<Setter Property="Grid.Column" Value="1"/>
<Style.Triggers>
<DataTrigger Binding="{Binding AlternativeLayout}" Value="True">
<Setter Property="Grid.Column" Value="0"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
Obviously this doesn't scale particularly well.
This is SO easy to do with ControlTemplates and DataTemplates but seems next to impossible when making UserControls. Is there something I'm missing?
You could define a ControlTemplate for the window or user control and define the triggers in the template:
<Window.DataContext>
<local:Window23ViewModel />
</Window.DataContext>
<Grid>
<UserControl>
<UserControl.Template>
<ControlTemplate>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding AlternativeLayout}" Value="True">
<Setter TargetName="Rect1" Property="Grid.Column" Value="1"/>
<Setter TargetName="Rect2" Property="Grid.Column" Value="0"/>
</DataTrigger>
</ControlTemplate.Triggers>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Rectangle x:Name="Rect1"
Grid.Column="0"
Width="200" Height="200"
Fill="Red"/>
<Rectangle x:Name="Rect2"
Grid.Column="1"
Width="200" Height="200"
Fill="Yellow"/>
<Button Grid.Row="1"
Grid.ColumnSpan="2"
Click="Button_Click">
Swap!
</Button>
</Grid>
</ControlTemplate>
</UserControl.Template>
</UserControl>
</Grid>
[ContentProperty(nameof(Setters))]
public class ContentTrigger : FrameworkElement
The application of your AttachedProperty can be simplified somewhat.
Let's change the implementation a bit:
[ContentProperty(nameof(Setters))]
public class ContentTrigger : FrameworkElement
{
#region ContentTriggerCollection Triggers dependency property
public static readonly DependencyProperty TriggersProperty = DependencyProperty.RegisterAttached(
"ShadowTriggers",
typeof(ContentTriggerCollection),
typeof(ContentTrigger),
new FrameworkPropertyMetadata(
null,
OnTriggersChanged));
public static ContentTriggerCollection GetTriggers(DependencyObject obj)
{
var value = (ContentTriggerCollection)obj.GetValue(TriggersProperty);
if (value == null)
SetTriggers(obj, value = new ContentTriggerCollection());
return value;
}
public static void SetTriggers(DependencyObject obj, ContentTriggerCollection value)
{
obj.SetValue(TriggersProperty, value);
}
private static void OnTriggersChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (args.OldValue is ContentTriggerCollection oldTriggers)
oldTriggers.SetApply(null);
if (args.NewValue is ContentTriggerCollection newTriggers)
newTriggers.SetApply(obj);
}
#endregion
public BindingBase Binding { get; set; }
public object Value { get; set; }
public SetterBaseCollection Setters { get; } = new SetterBaseCollection();
#region object ActualValue dependency property
internal static readonly DependencyProperty ActualValueProperty = DependencyProperty.Register(
"ActualValue",
typeof(object),
typeof(ContentTrigger),
new PropertyMetadata(
(object)null,
(obj, args) =>
{
((ContentTrigger)obj).OnActualValueChanged(args);
}));
private void OnActualValueChanged(DependencyPropertyChangedEventArgs args)
{
if (TestIsTriggered(args.NewValue))
ExecuteTrigger();
else
RestoreValues();
}
#endregion
private bool TestIsTriggered(object newValue)
{
if (newValue is bool b)
return b && (this.Value as string == "True" || this.Value as string == "true") ||
!b && (this.Value as string == "False" || this.Value as string == "false");
else
return object.Equals(this.Value, newValue);
}
public void Apply(DependencyObject obj)
{
if (!(obj is FrameworkElement fe))
return;
_target = fe;
BindingOperations.SetBinding(this, DataContextProperty, new Binding
{
Source = fe,
Path = new PropertyPath(DataContextProperty)
});
BindingOperations.SetBinding(this, ActualValueProperty, this.Binding);
}
private void ExecuteTrigger()
{
if (_target == null || _isTriggered)
return;
foreach (var setterBase in this.Setters)
{
if (!(setterBase is Setter setter) || string.IsNullOrEmpty(setter.TargetName))
continue;
var targetElem = GetTargetElement(setter.TargetName);
if (targetElem == null)
continue;
_originalValues[(targetElem, setter.Property)] = targetElem.GetValue(setter.Property);
targetElem.SetCurrentValue(setter.Property, ResolveSetterValue(setter));
}
_isTriggered = true;
}
private void RestoreValues()
{
if (_target == null || !_isTriggered)
return;
foreach (var setterBase in this.Setters)
{
if (!(setterBase is Setter setter) || string.IsNullOrEmpty(setter.TargetName))
continue;
var targetElem = GetTargetElement(setter.TargetName);
if (targetElem == null ||
// Value changed some other way since trigger?
targetElem.GetValue(setter.Property) != ResolveSetterValue(setter))
continue;
object restoredValue;
if (_originalValues.TryGetValue((targetElem, setter.Property), out restoredValue))
{
targetElem.SetCurrentValue(setter.Property, restoredValue);
}
}
_isTriggered = false;
}
private FrameworkElement GetTargetElement(string name)
{
FrameworkElement targetElem;
if (!_targetElements.TryGetValue(name, out targetElem))
{
targetElem = _target.FindName(name) as FrameworkElement;
if (targetElem != null)
_targetElements[name] = targetElem;
}
return targetElem;
}
private object ResolveSetterValue(Setter setter)
{
if (setter.Value is DynamicResourceExtension dr)
return _target.FindResource(dr.ResourceKey);
return setter.Value;
}
private Dictionary<(FrameworkElement, DependencyProperty), object> _originalValues =
new Dictionary<(FrameworkElement, DependencyProperty), object>();
private Dictionary<string, FrameworkElement> _targetElements = new Dictionary<string, FrameworkElement>();
private bool _isTriggered = false;
private FrameworkElement _target;
}
public class ContentTriggerCollection : Collection<ContentTrigger>
{
public DependencyObject Apply { get; private set; }
public void SetApply(DependencyObject apply)
{
Apply = apply;
foreach (ContentTrigger trigger in this)
{
if (trigger != null)
trigger.Apply(apply);
}
}
protected override void ClearItems()
{
foreach (ContentTrigger trigger in this)
{
if (trigger != null)
trigger.Apply(null);
}
base.ClearItems();
}
protected override void InsertItem(int index, ContentTrigger item)
{
base.InsertItem(index, item);
if (item != null)
item.Apply(Apply);
}
protected override void RemoveItem(int index)
{
if (this[index] is ContentTrigger removeTrigger)
removeTrigger.Apply(null);
base.RemoveItem(index);
}
protected override void SetItem(int index, ContentTrigger item)
{
if (this[index] is ContentTrigger removeTrigger)
removeTrigger.Apply(null);
base.SetItem(index, item);
if (item != null)
item.Apply(Apply);
}
}
<local:ContentTrigger.Triggers>
<local:ContentTrigger Binding="{Binding AlternativeLayout}" Value="True">
<Setter TargetName="Rect1" Property="Grid.Column" Value="1"/>
<Setter TargetName="Rect1" Property="Rectangle.Fill" Value="Green"/>
<Setter TargetName="Rect1" Property="Rectangle.Stroke" Value="Purple"/>
<Setter TargetName="Rect2" Property="Grid.Column" Value="0"/>
<Setter TargetName="Rect2" Property="Rectangle.Fill" Value="Gray"/>
<Setter TargetName="Rect2" Property="Rectangle.Stroke" Value="Black"/>
</local:ContentTrigger>
</local:ContentTrigger.Triggers>
The key change is registering the name "ShadowTriggers", which is different from the name of the Get and Set methods.
This registration does not allow XAML to refer to the AttachedProperty without going through the getter and setter.
Eldhasp gave me an idea:
[ContentProperty(nameof(Setters))]
public class ContentTrigger : FrameworkElement
{
#region ContentTriggerCollection Triggers dependency property
public static readonly DependencyProperty TriggersProperty = DependencyProperty.RegisterAttached(
"Triggers",
typeof(ContentTriggerCollection),
typeof(ContentTrigger),
new FrameworkPropertyMetadata(
null,
OnTriggersChanged));
public static ContentTriggerCollection GetTriggers(DependencyObject obj)
{
return (ContentTriggerCollection)obj.GetValue(TriggersProperty);
}
public static void SetTriggers(DependencyObject obj, ContentTriggerCollection value)
{
obj.SetValue(TriggersProperty, value);
}
private static void OnTriggersChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var triggers = args.NewValue as ContentTriggerCollection;
foreach (var trigger in triggers)
trigger.Apply(obj);
}
#endregion
public BindingBase Binding { get; set; }
public object Value { get; set; }
public SetterBaseCollection Setters { get; } = new SetterBaseCollection();
#region object ActualValue dependency property
internal static readonly DependencyProperty ActualValueProperty = DependencyProperty.Register(
"ActualValue",
typeof(object),
typeof(ContentTrigger),
new PropertyMetadata(
(object)null,
(obj, args) =>
{
((ContentTrigger)obj).OnActualValueChanged(args);
}));
private void OnActualValueChanged(DependencyPropertyChangedEventArgs args)
{
if (TestIsTriggered(args.NewValue))
ExecuteTrigger();
else
RestoreValues();
}
#endregion
private bool TestIsTriggered(object newValue)
{
if (newValue is bool b)
return b && (this.Value as string == "True" || this.Value as string == "true") ||
!b && (this.Value as string == "False" || this.Value as string == "false");
else
return object.Equals(this.Value, newValue);
}
private void Apply(DependencyObject obj)
{
if (!(obj is FrameworkElement fe))
return;
_target = fe;
BindingOperations.SetBinding(this, DataContextProperty, new Binding
{
Source = fe,
Path = new PropertyPath(DataContextProperty)
});
BindingOperations.SetBinding(this, ActualValueProperty, this.Binding);
}
private void ExecuteTrigger()
{
if (_target == null || _isTriggered)
return;
foreach (var setterBase in this.Setters)
{
if (!(setterBase is Setter setter) || string.IsNullOrEmpty(setter.TargetName))
continue;
var targetElem = GetTargetElement(setter.TargetName);
if (targetElem == null)
continue;
_originalValues[(targetElem, setter.Property)] = targetElem.GetValue(setter.Property);
targetElem.SetCurrentValue(setter.Property, ResolveSetterValue(setter));
}
_isTriggered = true;
}
private void RestoreValues()
{
if (_target == null || !_isTriggered)
return;
foreach (var setterBase in this.Setters)
{
if (!(setterBase is Setter setter) || string.IsNullOrEmpty(setter.TargetName))
continue;
var targetElem = GetTargetElement(setter.TargetName);
if (targetElem == null ||
// Value changed some other way since trigger?
targetElem.GetValue(setter.Property) != ResolveSetterValue(setter))
continue;
object restoredValue;
if (_originalValues.TryGetValue((targetElem, setter.Property), out restoredValue))
{
targetElem.SetCurrentValue(setter.Property, restoredValue);
}
}
_isTriggered = false;
}
private FrameworkElement GetTargetElement(string name)
{
FrameworkElement targetElem;
if (!_targetElements.TryGetValue(name, out targetElem))
{
targetElem = _target.FindName(name) as FrameworkElement;
if (targetElem != null)
_targetElements[name] = targetElem;
}
return targetElem;
}
private object ResolveSetterValue(Setter setter)
{
if (setter.Value is DynamicResourceExtension dr)
return _target.FindResource(dr.ResourceKey);
return setter.Value;
}
private Dictionary<(FrameworkElement, DependencyProperty), object> _originalValues =
new Dictionary<(FrameworkElement, DependencyProperty), object>();
private Dictionary<string, FrameworkElement> _targetElements = new Dictionary<string, FrameworkElement>();
private bool _isTriggered = false;
private FrameworkElement _target;
}
public class ContentTriggerCollection : Collection<ContentTrigger>
{
}
Usage:
<Window x:Class="TriggerFun.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:TriggerFun"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<local:ContentTrigger.Triggers>
<local:ContentTriggerCollection>
<local:ContentTrigger Binding="{Binding AlternativeLayout}" Value="True">
<Setter TargetName="Rect1" Property="Grid.Column" Value="1"/>
<Setter TargetName="Rect1" Property="Rectangle.Fill" Value="Green"/>
<Setter TargetName="Rect1" Property="Rectangle.Stroke" Value="Purple"/>
<Setter TargetName="Rect2" Property="Grid.Column" Value="0"/>
<Setter TargetName="Rect2" Property="Rectangle.Fill" Value="Gray"/>
<Setter TargetName="Rect2" Property="Rectangle.Stroke" Value="Black"/>
</local:ContentTrigger>
</local:ContentTriggerCollection>
</local:ContentTrigger.Triggers>
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Rectangle x:Name="Rect1"
StrokeThickness="2"
Stroke="Blue"
Width="200" Height="200"
Grid.Column="0"
Fill="Red"/>
<Rectangle x:Name="Rect2"
StrokeThickness="2"
Stroke="Orange"
Width="200" Height="200"
Grid.Column="1"
Fill="Yellow"/>
<Button Grid.Row="1"
Grid.ColumnSpan="2"
Click="Button_Click">
Swap!
</Button>
</Grid>
Shame I can't derive from TriggerBase, would be cleaner. But this is as close as I think I can get to what I want.
I have a button with the following content:
<StackPanel Orientation="Horizontal">
<TextBlock Text="Connect"/>
<materialDesign:PackIcon Kind="Arrow"/>
</StackPanel>
I searched and found this: WPF Button content binding but I'm not sure how to apply the solution when I have all of the three: a Stackpanel, the PackIcon (object) and the Textblock.
I have this progressBar which I make it appear under the button:
<ProgressBar x:Name="XZ" Foreground="Black" Grid.Row="4" Grid.Column="1"
Visibility="{Binding Connecting, UpdateSourceTrigger=PropertyChanged, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}"
Value="50"
IsIndeterminate="True" />
I want to make it so when I click the button, instead of showing the ProgressBar where it is right now, to basically remove the Text and the PackIcon and place the ProgressBar in the button.
Actually changing in the controls could be done with Data Triggers; though that seems a bit over the top in this case.
I would just toggle the visibility of two controls:
<Grid>
<StackPanel Orientation="Horizontal" Visibility="{Binding Connecting, Converter={StaticResource BooleanToCollapsedConverter}}"">
<TextBlock Text="Connect"/>
<materialDesign:PackIcon Kind="Arrow"/>
</StackPanel>
<ProgressBar x:Name="XZ" Foreground="Black" Grid.Row="4" Grid.Column="1"
Visibility="{Binding Connecting, Converter={StaticResource BooleanToVisibilityConverter}}"
Value="50"
IsIndeterminate="True" />
</Grid>
That would be the content of your button. BooleanToCollapsedConverter is just the inverse of a VisibiltyToBooleanConverter; there are a number of ways to do it and is left as an exercise.
As an aside; UpdateSourceTrigger doesn't make any sense on a OneWay binding (it doesn't update the source!) and you don't even need that on visibility as that's not an input the user can change.
You could use a data template. Something like:
XAML:
<Window.Resources>
<DataTemplate DataType="{x:Type local:ButtonInfo}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Label Grid.Row="0" Content="Press me"></Label>
<Label Grid.Row="1" Content="{Binding Label}"></Label>
</Grid>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ProgressInfo}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<ProgressBar Height="30" Value="{Binding Progress}"></ProgressBar>
<Label Grid.Row="1" Content="{Binding Label}"></Label>
</Grid>
</DataTemplate>
</Window.Resources>
<Grid>
<Button Command="{Binding ProcessCommand}" Content="{Binding ButtonInfo}">
</Button>
</Grid>
C#:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
}
}
public class ViewModelBase:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class MainWindowViewModel:ViewModelBase
{
public MainWindowViewModel()
{
ButtonInfo = new ButtonInfo(){Label = "Button Info"};
ProcessCommand = new DelegateCommand(Process);
}
private ButtonInfo _buttonInfo;
public ButtonInfo ButtonInfo
{
get { return _buttonInfo; }
set
{
_buttonInfo = value;
OnPropertyChanged();
}
}
public DelegateCommand ProcessCommand { get; set; }
private async void Process()
{
ButtonInfo = new ProgressInfo(){Label = "Progress Info"};
await ProcessAsync();
}
private Task ProcessAsync()
{
return Task.Run(() =>
{
for (int i = 0; i < 100; i++)
{
Application.Current.Dispatcher.Invoke(() =>
{
ButtonInfo.Progress = i;
if (i==99)
{
ButtonInfo = new ButtonInfo(){Label = "Button Again"};
}
});
Thread.Sleep(100);
}
});
}
}
public class ButtonInfo:ViewModelBase
{
private string _label;
private int _progress;
private bool _isProcessing;
public string Label
{
get { return _label; }
set
{
_label = value;
OnPropertyChanged();
}
}
public int Progress
{
get { return _progress; }
set
{
_progress = value;
OnPropertyChanged();
}
}
public bool IsProcessing
{
get { return _isProcessing; }
set
{
_isProcessing = value;
OnPropertyChanged();
}
}
}
public class ProgressInfo : ButtonInfo { }
You can create a template for the button to achieve this, then reuse the template everywhere you want a button with loading as following:
<Button Width="120" Height="40" Tag="False" Name="loadingButton" Click="loadingButton_Click">
<Button.Template>
<ControlTemplate>
<Border Name="PART_Border" BorderBrush="Black" BorderThickness="1" CornerRadius="2" Background="Transparent">
<Grid Name="PART_Root">
<TextBlock Name="PART_Text" HorizontalAlignment="Center" VerticalAlignment="Center">Data</TextBlock>
<ProgressBar IsIndeterminate="True" Name="PART_Loading"></ProgressBar>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="Tag" Value="True">
<Setter TargetName="PART_Text" Property="Visibility" Value="Collapsed"></Setter>
<Setter TargetName="PART_Loading" Property="Visibility" Value="Visible"></Setter>
</Trigger>
<Trigger Property="Tag" Value="False" >
<Setter TargetName="PART_Text" Property="Visibility" Value="Visible"></Setter>
<Setter TargetName="PART_Loading" Property="Visibility" Value="Collapsed"></Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
And the event for button click would be:
private async void loadingButton_Click(object sender, RoutedEventArgs e)
{
loadingButton.Tag = true.ToString();//display loading
await Task.Run(() => { Thread.Sleep(4000); });//fake data loading
loadingButton.Tag = false.ToString();//hide loading
}
Note that you can also bind the Tag property for a property inside your view model if you where using MVVM pattern.
I have a custom DataGrid control.
The class as follows:
public static class CloneRowDataGridCommands
{
private static RoutedCommand _cloneRowRoutedCommand = new RoutedCommand();
public static RoutedCommand CloneRowRoutedCommand
{
get { return _cloneRowRoutedCommand; }
}
}
public class CloneRowDataGrid : DataGrid
{
protected Button CloneButton { get; set; }
public static readonly DependencyProperty ShowCloneColumnProperty =
DependencyProperty.Register("ShowCloneColumn",
typeof(bool),
typeof(CloneRowDataGrid),
new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnShowCloneColumnPropertyChanged));
private static void OnShowCloneColumnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
CloneRowDataGrid cloneRowDataGrid = d as CloneRowDataGrid;
if (cloneRowDataGrid != null)
{
cloneRowDataGrid.OnShowCloneColumnPropertyChanged((bool) e.OldValue, (bool) e.NewValue);
}
}
protected virtual void OnShowCloneColumnPropertyChanged(bool oldValue, bool newValue)
{
// Nothing right now
}
public static readonly RoutedEvent CloneEvent =
EventManager.RegisterRoutedEvent("Clone", RoutingStrategy.Bubble,
typeof(CloneRowRoutedEventHandler),
typeof (CloneRowDataGrid));
public event CloneRowRoutedEventHandler Clone
{
add { AddHandler(CloneEvent, value); }
remove { RemoveHandler(CloneEvent, value); }
}
protected virtual void RaiseCloneEvent()
{
// TODO: change "new object()" to the row data
CloneRowRoutedEventArgs args = new CloneRowRoutedEventArgs(CloneEvent, new object());
RaiseEvent(args);
}
static CloneRowDataGrid()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(CloneRowDataGrid),
new FrameworkPropertyMetadata(typeof(CloneRowDataGrid)));
}
public CloneRowDataGrid()
{
CommandBindings.Add(new CommandBinding(CloneRowDataGridCommands.CloneRowRoutedCommand,
ExecuteCloneRow, CanExecuteCloneRow));
}
private void CanExecuteCloneRow(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
private void ExecuteCloneRow(object sender, ExecutedRoutedEventArgs e)
{
// TODO: Clone row
RaiseCloneEvent(); // pass object row to the eventargs
}
public bool ShowCloneColumn
{
get { return (bool) GetValue(ShowCloneColumnProperty); }
set { SetValue(ShowCloneColumnProperty, value); }
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
// Do stuff
// TODO: Fix this to avoid memory lick if the event is already registered
// TODO: Or change to a command for the clone button
CloneButton = GetTemplateChild("PART_CloneButton") as Button;
if (CloneButton != null)
{
CloneButton.Click += OnCloneButtonClick;
}
}
private void OnCloneButtonClick(object sender, RoutedEventArgs routedEventArgs)
{
// Invoke another event that might be registered from the client
RaiseCloneEvent();
}
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
// Do stuff
}
protected override void OnColumnDisplayIndexChanged(DataGridColumnEventArgs e)
{
base.OnColumnDisplayIndexChanged(e);
}
}
public delegate void CloneRowRoutedEventHandler(object sender, CloneRowRoutedEventArgs e);
public class CloneRowRoutedEventArgs : RoutedEventArgs
{
public object RowObject { get; protected set; }
public CloneRowRoutedEventArgs(RoutedEvent routedEvent, object rowObject) : base(routedEvent)
{
RowObject = rowObject;
}
}
The style in Generic.xaml as follows.
<Style TargetType="{x:Type uiControls:CloneRowDataGrid}" BasedOn="{StaticResource {x:Type DataGrid}}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type uiControls:CloneRowDataGrid}">
<!-- Parameter should be the row -->
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer x:Name="DG_ScrollViewer" Focusable="false">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
When using the control.
<uiControls:CloneRowDataGrid ItemsSource="{Binding SomeListCustomers}" AutoGenerateColumns="False" CanUserAddRows="False" x:Name="GridCustom"
HorizontalScrollBarVisibility="Disabled" BorderThickness="1" BorderBrush="DimGray"
ColumnHeaderStyle="{StaticResource DataGridColumnHeaderStyle}" Margin="10,0,25,0" Height="200">
<DataGrid.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Focusable" Value="False"/>
</Style>
</DataGrid.CellStyle>
<DataGrid.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="White"/>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="Black"/>
<SolidColorBrush x:Key="{x:Static SystemColors.ActiveBorderColorKey}" Color="White"/>
</DataGrid.Resources>
<DataGrid.Columns>
<StaticResource ResourceKey="CloneRowButtonColumn"/>
<DataGridTemplateColumn Header="Name" Width="75" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Name}" Width="75" IsReadOnly="True" Focusable="False" IsTabStop="False"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="NickName" Width="100" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding NickName}" Width="100" IsReadOnly="True" Focusable="False" IsTabStop="False"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</uiControls:CloneRowDataGrid>
What I get in designer and runtime is just a gray border with no columns. If I change the type tp DataGrid in the XAML window it shows 3 columns.
Definitely I'm missing something. I don't need anything special in styling, I just want DataGrid appearance and then work with additional functionality.
The goal is to have a column fixed and visible when needed with a flag.
To make it work, I actually had to remove the Property="Template" section and now the DataGrid styling applies. Though it feels like a hack and most probably need to know more about DataGrid styling override.
<Style TargetType="{x:Type uiControls:CloneRowDataGrid}" BasedOn="{StaticResource {x:Type DataGrid}}">
</Style>
My requirement is Binding dynamic property to my Value (double) property of Custom control.But it not working as expected.
Below is the code :
Simple customcontrol part :
xaml
<Style TargetType="{x:Type local:CustomControl1}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CustomControl1}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<TextBox x:Name="PART_TextBox" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
c#
public class CustomControl1 : Control
{
public double? Value
{
get { return (double?)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
// Using a DependencyProperty as the backing store for Value. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(double?), typeof(CustomControl1), new PropertyMetadata(null, new PropertyChangedCallback(OnValueChanged), new CoerceValueCallback(CoerceValueChange)));
private static object CoerceValueChange(DependencyObject d, object baseValue)
{
return baseValue;
}
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((CustomControl1)d).OnValueChanged(e);
}
private void OnValueChanged(DependencyPropertyChangedEventArgs e)
{
if (tb != null && e.NewValue != null)
tb.Text = e.NewValue.ToString();
else
tb.Text = string.Empty;
}
static CustomControl1()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl1), new FrameworkPropertyMetadata(typeof(CustomControl1)));
}
TextBox tb = null;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
tb = base.GetTemplateChild("PART_TextBox") as TextBox;
}
}
here goes the usage :
xaml
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<Label Content="Double TextBox without Converter" />
<Label Content="Double TextBox with Converter" />
<Label Content="MS TextBox" />
<Button Content="Button" Click="Button_Click_1"/>
</StackPanel>
<StackPanel Grid.Column="1">
<cc:CustomControl1 Value="{Binding MyValue}" Height="40" Width="200" Background="Red"/>
<TextBox Height="30" Text="{Binding MyValue}" />
</StackPanel>
</Grid>
c#
public partial class MainWindow : Window, INotifyPropertyChanged
{
private dynamic myValue;
public dynamic MyValue
{
get { return myValue; }
set { myValue = value; RaisePropertyChanged("MyValue"); }
}
public MainWindow()
{
InitializeComponent();
this.myValue = 3;
this.DataContext = this;
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
this.myValue = 5;
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string name)
{
if (PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
the coerce's base value is always null,but if i use converter in binding it works as expected.
but i want to use like normal property , Please help me on this.
binding works perfectly if i use TextBox,it is not the issue ,, i'm facing issue while i use DP's in CustomControl
Regards,
Kumar
Found answer from here..............
http://social.msdn.microsoft.com/Forums/vstudio/en-US/7d0f0a90-b0b8-45c9-9ec9-76517c8ea4af/dynamic-property-not-working-well-with-dependencyproperties?forum=wpf
I've a custom control look like this:
generic.xaml
<Style TargetType="controls:MyControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:MyControl">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20" />
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<TextBox Grid.Row="0"
Text="{Binding ElementName=slider, Path=Value}" />
<Slider Grid.Row="1" Name="slider" Width="120"
Minimum="1" Maximum="12"
Value="{Binding Mode=TwoWay,
RelativeSource={RelativeSource TemplatedParent},
Path=Value}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
MyControl.cs
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value",
typeof(double),
typeof(MyControl),
new PropertyMetadata(0d, OnValueChanged));
public double Value
{
get { return (double)base.GetValue(ValueProperty); }
set { base.SetValue(ValueProperty, value); }
}
private static void OnValueChanged(DependencyObject source,
DependencyPropertyChangedEventArgs e)
{
MyControl myControl = (MyControl)source;
myControl.OnValueChanged((double)e.OldValue, (double)e.NewValue);
}
protected virtual void OnValueChanged(double oldValue, double newValue)
{
double coercedValue = CoerceValue(newValue);
if (coercedValue != newValue)
{
this.Value = coercedValue;
}
}
private double CoerceValue(double value)
{
double limit = 7;
if (value > limit)
{
return limit;
}
return value;
}
The TextBox is just a dummy to show the value.
Now when I add this control to an Application, I am able to set the Slider value greater than 7, although the value of my DependencyProperty is set to 7.
What I am doing wrong? Does the TwoWayBinding does not work in this situation?
Thanks in advance
Steps for my repro:-
Create a fresh new Silverlight Application in VS2010 call SilverlightApplication1.
Add new "Silverlight Templated Control" to the silverlight project, naming it "MyControl".
Copied the inner contents or you ControlTemplate into the ControlTemplate of the themes/Generic.xaml file. This Entire generic file looks like:-
<Style TargetType="local:MyControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:MyControl">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20" />
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<TextBox Grid.Row="0"
Text="{Binding ElementName=slider, Path=Value}" />
<Slider Grid.Row="1" Name="slider" Width="120"
Minimum="1" Maximum="12"
Value="{Binding Mode=TwoWay,
RelativeSource={RelativeSource TemplatedParent},
Path=Value}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Copied your C# placed in in MyControl.cs. The whole file looks like:-
using System.Windows;
using System.Windows.Controls;
namespace SilverlightApplication1
{
public class MyControl : Control
{
public MyControl()
{
this.DefaultStyleKey = typeof(MyControl);
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value",
typeof(double),
typeof(MyControl),
new PropertyMetadata(0d, OnValueChanged));
public double Value
{
get { return (double)base.GetValue(ValueProperty); }
set { base.SetValue(ValueProperty, value); }
}
private static void OnValueChanged(DependencyObject source,
DependencyPropertyChangedEventArgs e)
{
MyControl myControl = (MyControl)source;
myControl.OnValueChanged((double)e.OldValue, (double)e.NewValue);
}
protected virtual void OnValueChanged(double oldValue, double newValue)
{
double coercedValue = CoerceValue(newValue);
if (coercedValue != newValue)
{
this.Value = coercedValue;
}
}
private double CoerceValue(double value)
{
double limit = 7;
if (value > limit)
{
return limit;
}
return value;
}
}
}
Added an instance of MyControl to MainPage.xaml, which now looks like:-
<UserControl x:Class="SilverlightApplication1.MainPage"
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:SilverlightApplication1"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<Grid x:Name="LayoutRoot" Background="White">
<local:MyControl />
</Grid>
</UserControl>
Run the solution, works fine.