When I add my usercontrol to a window, why does the text vibrates in design mode?.
<UserControl x:Class="AircraftGauges.InnerGauge"
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:AircraftGauges"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="200"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<UserControl.Resources>
<local:CenterConverter x:Key="CenterConverter"/>
</UserControl.Resources>
<Canvas Width="200" Height="200" Background="White">
<Path Stroke="Black" StrokeThickness="1" Data="M 40, 95 V 105 H 160 V 95 H 105 V 85 H 95 V 95 Z " Fill="Black"/>
<TextBlock Name="CenterLabel" Text="{Binding PathData}" Foreground="Black" VerticalAlignment="Center"
Canvas.Left="100" Canvas.Top="112">
<TextBlock.Margin>
<MultiBinding Converter="{StaticResource CenterConverter}">
<Binding ElementName="CenterLabel" Path="ActualWidth"/>
<Binding ElementName="CenterLabel" Path="ActualHeight"/>
</MultiBinding>
</TextBlock.Margin>
</TextBlock>
</Canvas>
</UserControl>
Code Behind: InnerGauge.xaml.cs:
namespace AircraftGauges
{
/// <summary>
/// Interaction logic for InnerGauge.xaml
/// </summary>
public partial class InnerGauge : UserControl, INotifyPropertyChanged
{
private string _path;
public event PropertyChangedEventHandler PropertyChanged;
public InnerGauge()
{
InitializeComponent();
SizeChanged += SizeChangedEventHandler;
}
private void SizeChangedEventHandler(object sender, SizeChangedEventArgs e)
{
UpdatePathData(e.NewSize.Width, e.NewSize.Height);
}
private void UpdatePathData(double w, double h)
{
var newPath = $"W {w} -- H {h}";
if (String.CompareOrdinal(_path, newPath) != 0)
{
_path = newPath;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PathData)));
}
}
public string PathData
{
get
{
return _path;
}
}
}
}
CenterConverter.cs
namespace AircraftGauges
{
public class CenterConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values[0] == DependencyProperty.UnsetValue || values[1] == DependencyProperty.UnsetValue)
{
return DependencyProperty.UnsetValue;
}
var width = (double) values[0];
var height = (double) values[1];
return new Thickness(-width/2, -height/2, 0, 0);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
Interesting case indeed, and the reason is layout rounding. It seems that wpf designer by default sets UseLayoutRounding to true for the window you view in it. However, in runtime UseLayoutRounding is false for window. What layout rounding means is things like width\height\margins are rounded to whole pixel during layout measurment. In your CenterConverter you calculate margin of textblock based on it's own height and width. But changing margin causes width change, and that in turn causes margin change. Because of layout rounding this process never ends.
You can verify that by setting UseLayoutRounding = false explicitly (designer will stop vibrating) or setting to true explicitly (will vibrate in runtime). In general you need to fix CenterConverter to take that into account.
Related
I have a set of converters that are used dynamically position squares on a canvas. The squares are stored in an observable collection. CanvasPositionScaleConverter converts a value within a range to a value between 0 and 1. If the value it is converting is outside the specified range, it will throw an Argument exception.
My issue is when I clear my squares collection, whilst the screen becomes empty, "phantom" elements seem to be left behind which the converters continue to act on. Therefore, when the canvas is resized, I still get exceptions being thrown, even if the squares collection has been cleared.
Why are the converters still running on these elements that have been deleted through Squares.Clear()?
Note
Using Squares = new ObservableCollection<Square>() instead of Squares.Clear() still causes this issue.
Update
This issue does not happen if the Sqaures collection is never added too and Resize is pressed. It only happens if elements have been deleted from the collection (and obviously if haven't been removed from the collection but are outside the valid range).
The exception details are
Exception thrown: 'System.ArgumentException' in LateExceptionMcve.dll
An unhandled exception of type 'System.ArgumentException' occurred in LateExceptionMcve.dll
Value cannot be greater than max
MCVE
MainWindow.xaml
<Window x:Class="LateExceptionMcve.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:LateExceptionMcve"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=local:MainWindow, IsDesignTimeCreatable=True}"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Viewbox Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="0"
Stretch="Uniform">
<Border BorderThickness="1"
BorderBrush="Black">
<Border.Resources>
<local:CanvasScaleConverter x:Key="CanvasScaleConverter" />
<local:CanvasPositionScaleConverter x:Key="CanvasPositionScaleConverter" />
</Border.Resources>
<ItemsControl ItemsSource="{Binding Squares}"
Width="{Binding Size, Converter={StaticResource CanvasScaleConverter}}"
Height="{Binding Size, Converter={StaticResource CanvasScaleConverter}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left">
<Setter.Value>
<MultiBinding Converter="{StaticResource CanvasPositionScaleConverter}">
<Binding Path="X" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Min" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Max" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Size"
Converter="{StaticResource CanvasScaleConverter}" />
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Canvas.Top">
<Setter.Value>
<MultiBinding Converter="{StaticResource CanvasPositionScaleConverter}">
<Binding Path="Y" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Min" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Max" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Size"
Converter="{StaticResource CanvasScaleConverter}" />
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Width" Value="50" />
<Setter Property="Height" Value="50" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Background="Red" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</Viewbox>
<Button Grid.Column="0"
Grid.Row="1"
Content="Clear"
Click="Clear" />
<Button Grid.Column="1"
Grid.Row="1"
Content="Resize"
Click="Resize" />
</Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Data;
namespace LateExceptionMcve
{
public partial class MainWindow : INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
Min = 0;
Max = 10;
Size = Max - Min;
Squares = new ObservableCollection<Square>
{
new Square
{
X = 8,
Y = 7,
},
};
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void Resize(object sender, RoutedEventArgs e)
{
Min = 0;
Max = 5;
Size = Max - Min;
}
private void Clear(object sender, RoutedEventArgs e)
{
Squares.Clear();
}
private ObservableCollection<Square> _squares;
public ObservableCollection<Square> Squares
{
get => _squares;
set
{
_squares = value;
OnPropertyChanged();
}
}
private double _size;
public double Size
{
get => _size;
set
{
_size = value;
OnPropertyChanged();
}
}
private double _min;
public double Min
{
get => _min;
set
{
_min = value;
OnPropertyChanged();
}
}
private double _max;
public double Max
{
get => _max;
set
{
_max = value;
OnPropertyChanged();
}
}
}
public sealed class Square : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private double _x;
public double X
{
get => _x;
set
{
_x = value;
OnPropertyChanged();
}
}
private double _y;
public double Y
{
get => _y;
set
{
_y = value;
OnPropertyChanged();
}
}
}
public sealed class CanvasScaleConverter : IValueConverter
{
private const double Scale = 100;
public object Convert(
object value,
Type targetType,
object parameter,
CultureInfo culture)
{
if (value is double valueToScale)
{
return valueToScale * Scale;
}
return value;
}
public object ConvertBack(
object value,
Type targetType,
object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}
public sealed class CanvasPositionScaleConverter : IMultiValueConverter
{
public object Convert(
object[] values,
Type targetType,
object parameter,
CultureInfo culture)
{
if (values.Length == 4 &&
values[0] is double value &&
values[1] is double min &&
values[2] is double max &&
values[3] is double canvasWidthHeight)
{
return canvasWidthHeight * RangeToNormalizedValue(min, max, value);
}
return values;
}
private static double RangeToNormalizedValue(
double min,
double max,
double value)
{
if (min > max)
{
throw new ArgumentException("Min cannot be less than max");
}
if (value < min)
{
throw new ArgumentException("Value cannot be less than min");
}
if (value > max)
{
throw new ArgumentException("Value cannot be greater than max");
}
return (value - min) / (max - min);
}
public object[] ConvertBack(
object value,
Type[] targetTypes,
object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}
}
Edit
I have filled a bug report on the WPF GitHub repo, so hopefully that may help find what is going on here.
Information from miloush on the GitHub issue.
The cause of this issue is that even though the Squares collection has been cleared and the objects are no longer visible on the screen, the objects have not been garbage collected.
It is possible to force the garbage collection to happen by modifying the Clear method to something like
private void Clear(object sender, RoutedEventArgs e)
{
Squares.Clear();
Dispatcher.BeginInvoke(new Action(GC.Collect), DispatcherPriority.ContextIdle);
}
And this will (eventually) clear the objects away. However, it is not immediate. Even if just GC.Collect() after Squares.Clear had been called it is not immediate.
This issue could occur at many times in WPF, including in common actions such as virtualisation. Therefore it would be better to update the converter so it can handle invalid inputs gracefully.
public sealed class CanvasPositionScaleConverter : IMultiValueConverter
{
public object Convert(
object[] values,
Type targetType,
object parameter,
CultureInfo culture)
{
if (values.Length != 4 ||
!(values[0] is double value) ||
!(values[1] is double min) ||
!(values[2] is double max) ||
!(values[3] is double canvasWidthHeight))
{
return null;
}
try
{
return canvasWidthHeight * RangeToNormalizedValue(min, max, value);
}
catch (ArgumentException e) when (e.Message == ValueLessThanMin)
{
return 0;
}
catch (ArgumentException e) when (e.Message == ValueGreaterThanMax)
{
return canvasWidthHeight;
}
}
// This is in a utility class normally
private const string ValueLessThanMin = "Value cannot be less than min";
private const string ValueGreaterThanMax = "Value cannot be greater than max";
private static double RangeToNormalizedValue(
double min,
double max,
double value)
{
if (min > max)
{
throw new ArgumentException("Min cannot be less than max");
}
if (value < min)
{
throw new ArgumentException(ValueLessThanMin);
}
if (value > max)
{
throw new ArgumentException(ValueGreaterThanMax);
}
return (value - min) / (max - min);
}
public object[] ConvertBack(
object value,
Type[] targetTypes,
object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}
Hello I'm creating polyline with dots(elipse) which will be updated by user mouse move after he clicked on dot, problem is that dot is moving which is good, but does not update source which is ObservableCollection
MainWindow.xaml
<Window x:Class="sampleWPF.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:l="clr-namespace:sampleWPF" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800" Background="Green" MouseRightButtonDown="MainWindow_OnMouseRightButtonDown_">
<Window.Resources>
<l:PointCollectionConverter x:Key="pointCollectionConverter" />
</Window.Resources>
<Grid>
<Canvas x:Name="Canvas" Background="Yellow" Width="300" Height="300" MouseMove="UIElement_OnMouseMove">
<Polyline x:Name="polyline" Stroke="Black" Width="300" Height="300" Points="{Binding PointCollection, Converter={StaticResource pointCollectionConverter}}" />
<ItemsControl ItemsSource="{Binding PointCollection}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}"/> <Setter Property="Canvas.Top" Value="{Binding Y}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Width="10" Height="10" Fill="Blue" MouseLeftButtonDown="UIElement_OnMouseLeftButtonDown" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
</Grid>
Some explanation:
MainWindow_OnMouseRightButtonDown_ stops selected elipse move.
UIElement_OnMouseMove moves elipse in canvas
UIElement_OnMouseLeftButtonDown selects elipse which will be moved
MainWindow.xaml.cs
public partial class MainWindow: INotifyPropertyChanged {
public MainWindow() {
InitializeComponent();
DataContext = this;
PointCollection.Add(new Point(0, 0));
PointCollection.Add(new Point(20, 20));
PointCollection.Add(new Point(30, 30));
PointCollection.Add(new Point(50, 50));
PointCollection.Add(new Point(80, 80));
PointCollection.Add(new Point(100, 100));
}
private ObservableCollection < Point > PointCollectionProperty {
get;
set;
} = new ObservableCollection < Point > ();
public ObservableCollection < Point > PointCollection {
get => PointCollectionProperty;
set {
PointCollectionProperty = value;
OnPropertyChanged(nameof(PointCollection));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string caller) {
PropertyChanged ? .Invoke(this, new PropertyChangedEventArgs(caller));
}
Point point;
private Point selectedPoint;
bool activated = false;
private Ellipse selectedEllipse;
private ContentPresenter selectedContentPresenter;
private void UIElement_OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
if (sender is Ellipse es) {
Mouse.Capture(es);
selectedEllipse = es;
var parent = VisualTreeHelper.GetParent(selectedEllipse);
if (parent != null) {
selectedContentPresenter = parent as ContentPresenter;
}
point = Mouse.GetPosition(selectedContentPresenter);
if (PointCollection.Any(t => t.X == Canvas.GetLeft(selectedContentPresenter) && t.Y == Canvas.GetTop(selectedContentPresenter))) {
Debug.WriteLine("found");
selectedPoint = PointCollection.FirstOrDefault(t =>
t.X == Canvas.GetLeft(selectedContentPresenter) && t.Y == Canvas.GetTop(selectedContentPresenter));
} else {
selectedPoint = new Point();
Debug.WriteLine("not found");
}
activated = true;
}
}
private void UIElement_OnMouseMove(object sender, MouseEventArgs e) {
if (selectedContentPresenter != null) {
if (activated) {
double top = Canvas.GetTop(selectedContentPresenter) + Mouse.GetPosition(selectedContentPresenter).Y + point.Y;
double left = Canvas.GetLeft(selectedContentPresenter) + Mouse.GetPosition(selectedContentPresenter).X + point.X;
Canvas.SetTop(selectedContentPresenter, top);
Canvas.SetLeft(selectedContentPresenter, left);
selectedPoint.X = left;
selectedPoint.Y = top;
OnPropertyChanged(nameof(PointCollection));
}
}
}
private void MainWindow_OnMouseRightButtonDown_(object sender, MouseButtonEventArgs e) {
foreach(Point child in PointCollection) {
Debug.WriteLine(child.X + " " + child.Y);// shows original values from public MainWindow()
}
Mouse.Capture(null);
activated = false;
selectedEllipse = null;
selectedContentPresenter = null;
}
}
public class PointCollectionConverter: IValueConverter {
Debug.WriteLine("Called");
public object Convert(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture) {
if (value.GetType() == typeof(ObservableCollection < Point > ) && targetType == typeof(PointCollection)) {
var pointCollection = new PointCollection();
foreach(var point in value as ObservableCollection < Point > ) pointCollection.Add(point);
return pointCollection;
}
return null;
}
public object ConvertBack(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture) {
return null;
}
}
What I'm missing here since elipse does move but polyline stays untouched
Edit: added Debug.WriteLine to show if its working, but spams debug but collection stays still the same
Image proving that it breaks after mouse move
I have an ObservableCollection'<'NextBestAction'>' NextBestActions called NextBestActions, where NextBestAction is:
[TypeConverter(typeof(NextBestActionTypeConverter))]
public class NextBestAction : IDisposable
{
public bool isDismissable, dismissed, completed;
public NextBestActionType type;
public string title, description;
public void Dispose()
{
this.Dispose();
}
public NextBestAction()
{
}
public NextBestAction(string title, string description)
{
this.title = title;
this.description = description;
}
public static NextBestAction Parse(Card card)
{
if (card == null)
{
return new NextBestAction();
}
return new NextBestAction(card.Title.Text, card.Description.Text);
}
}
I also have my own UserControl called Card, where Card is:
public partial class Card : UserControl
{
public Card()
{
InitializeComponent();
this.DataContext = this;
}
public Card(string title, string description)
{
InitializeComponent();
this.DataContext = this;
this.Title.Text = title;
this.Description.Text = description;
}
public static Card Parse(NextBestAction nextBestAction)
{
if (nextBestAction == null)
{
return new Card();
}
return new Card(nextBestAction.title, nextBestAction.description);
}
}
with the folling XAML:
<UserControl x:Class="AdvancedTeller.Card"
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:AdvancedTeller"
mc:Ignorable="d"
d:DesignWidth="300" Background="White" BorderBrush="#FF333333" VerticalContentAlignment="Top" Width="400">
<Grid Margin="10" VerticalAlignment="Top">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Name="Title" Grid.Column="1" Grid.Row="0" FontSize="18.667" Margin="3"/>
<TextBlock Name="Description" Grid.Column="1" Grid.Row="1" VerticalAlignment="Top" TextWrapping="Wrap" Margin="3"/>
</Grid>
Finally, I have defined a TypeConverter for NextBestAction as
public class NextBestActionTypeConverter : TypeConverter
{
// Override CanConvertFrom to return true for Card-to-NextBestAction conversions.
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(Card))
{
return true;
}
return base.CanConvertFrom(context, sourceType);
}
// Override CanConvertTo to return true for NextBestAction-to-Card conversions.
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(Card))
{
return true;
}
return base.CanConvertTo(context, destinationType);
}
// Override ConvertFrom to convert from a Card to an instance of NextBestAction.
public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
{
Card card = value as Card;
if (card != null)
{
try
{
return NextBestAction.Parse(card);
}
catch (Exception e)
{
throw new Exception(String.Format("Cannot convert '{0}' ({1}) because {2}", value, value.GetType(), e.Message), e);
}
}
return base.ConvertFrom(context, culture, value);
}
// Override ConvertTo to convert from an instance of NextBestAction to Card.
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
{
if (destinationType == null)
{
throw new ArgumentNullException("destinationType");
}
//Convert Complex to a string in a standard format.
NextBestAction nextBestAction = value as NextBestAction;
if (nextBestAction != null && this.CanConvertTo(context, destinationType) && destinationType == typeof(Card))
{
try
{
return Card.Parse(nextBestAction);
}
catch (Exception e)
{
throw new Exception(String.Format("Cannot convert '{0}' ({1}) because {2}", value, value.GetType(), e.Message), e);
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
I am trying to bind the NextBestActions to a StackPanel, and force the NextBestActions to be represented in the UI as Cards.
So far, I have understood that I need at least this
<StackPanel Grid.Column="1" Grid.Row="1" Grid.RowSpan="2" Margin="50" >
<ItemsControl Name="NextBestActionItems" ItemsSource="{Binding NextBestActions}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<AdvancedTeller:Card />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
The code compiles and runs without any issues, and a Card is created and visible in the StackPanel for each item in the ObservableCollection, however, each Card's Title and Description are blank and won't take the data of their respective NextBestAction.
I feel like I'm 90% of the way there. I would appreciate any help. Thanks!
UPDATE/EDIT 1: Currently the NextBestActionTypeConverter is not called/hit. If I remove the ItemsControl.ItemTemplate definition from the XAML, then the NextBestActionTypeConverter is called, but with destinationType as "string". I am trying to force/set the ItemsControl to understand that the itesm are to be represented as Cards.
UPDATE/EDIT 2 (Answer): Here are the snippets for the answer:
// Override ConvertTo to convert from an instance of NextBestAction to Card.
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
{
if (destinationType == null)
{
throw new ArgumentNullException("destinationType");
}
//Convert Complex to a string in a standard format.
NextBestAction nextBestAction = value as NextBestAction;
if (nextBestAction != null && this.CanConvertTo(context, destinationType) && destinationType == typeof(Card))
{
try
{
return new Card();
}
catch (Exception e)
{
throw new Exception(String.Format("Cannot convert '{0}' ({1}) because {2}", value, value.GetType(), e.Message), e);
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
and
public partial class Card : UserControl
{
public Card()
{
InitializeComponent();
}
}
and
<UserControl x:Class="AdvancedTeller.Card"
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:AdvancedTeller"
mc:Ignorable="d"
d:DesignWidth="300" Background="White" BorderBrush="#FF333333" VerticalContentAlignment="Top" Width="400">
<Grid Margin="10" VerticalAlignment="Top">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Name="Title" Grid.Column="1" Grid.Row="0" FontSize="18.667" Margin="3" Text="{Binding Title}"/>
<TextBlock Name="Description" Grid.Column="1" Grid.Row="1" VerticalAlignment="Top" TextWrapping="Wrap" Margin="3" Text="{Binding Description}"/>
</Grid>
There's a few potential problem spots in your code that I can see.
First of all, it looks like you do not have full properties, with get; set; accessor methods, defined for Title and Description in your NextBestAction object.
If I recall correctly, WPF's binding system requires full properties with get/set accessors, and does not bind to fields that don't have them.
So
public string title, description;
Should become
public string Title { get; set; }
public string Description { get; set; }
Another potential problem may be the fact you haven't bound the .Text property of your Title/Description textboxes in the Card UserControl.
So assuming you aren't using a framework that automatically creates Bindings based on the .Name property like Caliburn Micro, this code
<TextBlock Name="Title" ... />
<TextBlock Name="Description" ... />
Should be
<TextBlock Name="Title" Text="{Binding Title}" ... />
<TextBlock Name="Description" Text="{Binding Description}" ... />
Also, I'm pretty sure bindings are case-sensitive, so you want to be sure the case of your Bindings (or Name property if using Caliburn Micro) matches the case of your Properties.
And last of all, anytime I see a UserControl hardcode it's .DataContext, warning bells go off in my head. UserControls should not do this. They should get their DataContext from whatever code is using the control, and should not create their own. Get rid of the following lines of code that hardcodes the .DataContext in the Card UserControl, and then it will use whatever values are in the NextBestActions collection instead.
public Card()
{
InitializeComponent();
this.DataContext = this; // Bad!
}
public Card(string title, string description)
{
InitializeComponent();
this.DataContext = this; // Bad!
this.Title.Text = title; // Should be replaced with bindings as above
this.Description.Text = description; // Should be replaced with bindings as above
}
(As a side note, I have no idea what you're doing with that TypeConverter :) It's typically used to convert one type to another, such as changing the string "Red" to a SolidColorBrush with a .Color set to Red when you type something like <StackPanel Background="Red" />. I don't see a use for it with your current code and would recommend getting rid of it entirely unless you need it for some specific reason.)
Hello I am trying to make a custom Button with PathData inside it. So far I have managed to view the Path inside it. But my Button is not taking MVVM Commands.
Custom Button XAML
<Button x:Class="My_Class.CustomControls.MyButtonWithPath"
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"
mc:Ignorable="d"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
d:DesignHeight="480" d:DesignWidth="480">
<Grid>
<Path Data="{Binding}" Name="path"/>
</Grid>
</Button>
Button Code Behind
public partial class MyButtonWithPath : Button
{
public MyButtonWithPath()
{
InitializeComponent();
this.DataContext = this;
}
private static string _pathDatay;
public string PathDatay
{
get { return _pathDatay; }
set { _pathDatay = value; }
}
public static readonly DependencyProperty PathDataProperty =
DependencyProperty.Register("PathDatay", typeof(string), typeof(MyButtonWithPath), new PropertyMetadata(pathDataCallback));
private static void pathDataCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
_pathDatay = (string)e.NewValue;
}
}
Usage In XAML
<converters:KeyToValueConverter x:Key="conv"/>
<custom:MyButtonWithPath PathDatay="{Binding ConverterParameter=tv, Converter={StaticResource conv}}" />
Converter
public class KeyToValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
try
{
return Utils.GetPathData(parameter.ToString());
}
catch (Exception c)
{
throw c;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new System.NotImplementedException();
}
}
Utils Class
public static string GetPathData(string key)
{
Dictionary<string, string> dictionary = new Dictionary<string, string>();
dictionary.Add("tv", "etc etc etc......");
return dictionary[key];
}
The Problem
Whenever I am writing a Command in the Usage of the Button, it shows a not found error as it looks for the command inside my Custom Button NOT my MainViewModel(FYI I have a ViewModel named MainViewModel where I will put the Command related codes)
My Guess
I am setting the DataContext of the Button to itself with this "this.DataContext=this;" But if I omit this line then the Path does not show. Please guide me to set these things correctly
Solution
Given below
You are right, this.DataContext=this; this line is the problem.
You typically dont use databinding for setting the look of the custom control. Instead you should set the Name of Path and set its Data property on initialization. You can do this by overriiding OnApplyTemplate:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
Path p = GetTemplateChild("path") as Path;
p.Data = ...
}
I messed up with the code, #Domysee guided me the right way. My corrections--
Custom Button XAML (Not Needed)
Button Code Behind, Usage In XAML, Converter, Utils (Same as Before)
Additional Part (StyleTemplate)
<Style x:Key="MyButtonWithPathStyle" TargetType="CustomControls:MyButtonWithPath">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CustomControls:MyButtonWithPath">
<Grid Background="Transparent">
<Path x:Name="path" Data="{Binding}" DataContext="{TemplateBinding PathDatay}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
That's it! DataContext="{TemplateBinding PathDatay}" did the trick! Thanks everyone
I'm using a System.Windows.Controls.DataGrid (WPF, .NET 4.0, C#). If a cell's validation fails (HasErrors == TRUE) the OK button should be gray (IsEnabled = FALSE).
The DataGrid validation is performed using a ValidationRule.
I've read several closely related articles here on StackOverflow, but I'm still jammed. I think the problem is that the Validation is on the DataGridRow, but the OK button's IsEnabled binding is looking at the whole grid.
To see the error, add a third row to the grid, and put in an invalid number (e.g. 200).) Or just edit one of the two stock values to be invalid (less than 0, non-integer, or greater than 100).
Here is the xaml:
<Window x:Class="SimpleDataGridValidation.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:SimpleDataGridValidation"
Title="MainWindow"
Height="350"
Width="525">
<Window.Resources>
<c:BoolToOppositeBoolConverter x:Key="boolToOppositeBoolConverter" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<DataGrid Name="myDataGrid"
ItemsSource="{Binding}"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Time"
x:Name="TimeField">
<DataGridTextColumn.Binding>
<Binding Path="Time"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<c:TimeValidationRule />
</Binding.ValidationRules>
</Binding>
</DataGridTextColumn.Binding>
</DataGridTextColumn>
</DataGrid.Columns>
<DataGrid.RowValidationErrorTemplate>
<ControlTemplate>
<Grid Margin="0,-2,0,-2"
ToolTip="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGridRow}},
Path=(Validation.Errors)[0].ErrorContent}">
<Ellipse StrokeThickness="0"
Fill="Red"
Width="{TemplateBinding FontSize}"
Height="{TemplateBinding FontSize}" />
<TextBlock Text="!"
FontSize="{TemplateBinding FontSize}"
FontWeight="Bold"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
</DataGrid.RowValidationErrorTemplate>
</DataGrid>
<Button Name="btnOk"
Width="40"
Content="_OK"
IsDefault="True"
Grid.Row="1"
Click="btnOk_Click"
IsEnabled="{Binding ElementName=myDataGrid, Path=(Validation.HasError), Converter={StaticResource boolToOppositeBoolConverter}}">
</Button>
</Grid>
</Window>
And here is the C# code-behind:
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace SimpleDataGridValidation
{
public partial class MainWindow : Window
{
public ObservableCollection<CTransition> Transitions;
public MainWindow()
{
InitializeComponent();
Transitions = new ObservableCollection<CTransition>();
Transitions.Add(new CTransition(10, 5));
Transitions.Add(new CTransition(20, 7));
myDataGrid.DataContext = Transitions;
}
private void btnOk_Click(object sender, RoutedEventArgs e)
{
this.Close();
}
}
public class CTransition
{
public int Time { get; set; }
public int Speed { get; set; }
public CTransition()
{ }
public CTransition(int thetime, int thespeed)
{
Time = thetime;
Speed = thespeed;
}
}
public class TimeValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
if (value != null)
{
int proposedValue;
if (!int.TryParse(value.ToString(), out proposedValue))
{
return new ValidationResult(false, "'" + value.ToString() + "' is not a whole number.");
}
if (proposedValue > 100)
{
return new ValidationResult(false, "Maximum time is 100 seconds.");
}
if (proposedValue < 0)
{
return new ValidationResult(false, "Time must be a positive integer.");
}
}
// Everything OK.
return new ValidationResult(true, null);
}
}
[ValueConversion(typeof(Boolean), typeof(Boolean))]
public class BoolToOppositeBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (targetType != typeof(bool) && targetType != typeof(System.Nullable<bool>))
{
throw new InvalidOperationException("The target must be a boolean");
}
if (null == value)
{
return null;
}
return !(bool)value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (targetType != typeof(bool) && targetType != typeof(System.Nullable<bool>))
{
throw new InvalidOperationException("The target must be a boolean");
}
if (null == value)
{
return null;
}
return !(bool)value;
}
}
}
I got it to work by adding 3 things:
"NotifyOnValidationError = True" in DataGridTextColumn's Binding
AddHandler(Validation.ErrorEvent, new RoutedEventHandler(OnErrorEvent)); - Added to MainWindow's constructor.
OnErrorEvent function that keeps track of the error counts and sets the OK button's IsEnabled status. - See Disable/enable button with DataGridTextColumn validation and Using WPF Validation rules and disabling a 'Save' button