I'm trying to position elements in my Canvas relative to my background.
Window is re-sized keeping the aspect ratio.
Background is stretched with window size.
The problem is once window is re-sized the element positions are incorrect. If window is re-sized just a little, elements will adjust their size a bit and would be still in the correct position, but if window is re-sized to double it's size then positioning is completely off.
So far I used Grid, but it was to no avail as well. Here is the XAML
<Window x:Class="CanvasTEMP.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" ResizeMode="CanResizeWithGrip" SizeToContent="WidthAndHeight" MinHeight="386" MinWidth="397.5" Name="MainWindow1"
xmlns:c="clr-namespace:CanvasTEMP" Loaded="onLoad" WindowStartupLocation="CenterScreen" Height="386" Width="397.5" WindowStyle="None" AllowsTransparency="True" Topmost="True" Opacity="0.65">
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas Height="77" Width="218">
<Label Content="{Binding OwnerData.OwnerName}" Height="36" Canvas.Left="8" Canvas.Top="55" Width="198" Padding="0" HorizontalAlignment="Left" HorizontalContentAlignment="Center" VerticalAlignment="Center"/>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas>
<Canvas.Background>
<ImageBrush ImageSource="Resources\default_mapping.png" Stretch="Uniform"/>
</Canvas.Background>
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding OwnerData.left}" />
<Setter Property="Canvas.Top" Value="{Binding OwnerData.top}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
Class that is used for data binding
public class Owner : INotifyPropertyChanged
{
public double _left;
public double _top;
public string OwnerName { get; set; }
public double top { get { return _top; }
set
{
if (value != _top)
{
_top = value;
OnPropertyChanged();
}
}
}
public double left
{
get { return _left; }
set
{
if (value != _left)
{
_left = value;
OnPropertyChanged();
}
}
}
public string icon { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = "none passed")
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public class ForDisplay
{
public Owner OwnerData { get; set; }
public int Credit { get; set; }
}
And here is the code that is run every second to keep elements' position relative to window size
items[0].OwnerData.left = this.Width * (10 / Defaul_WindowSize_Width);
items[0].OwnerData.top = this.Height * (55 / Defaul_WindowSize_Height);
10 and 50 are default Canvas.Left and Canvas.Top that are used when window is first initialized.
Would appreciate if anyone can point out what I'm doing wrong.
You need an attach property:
public static readonly DependencyProperty RelativeProperty =
DependencyProperty.RegisterAttached("Relative", typeof(double), typeof(MyControl));
public static double GetRelative(DependencyObject o)
{
return (double)o.GetValue(RelativeProperty);
}
public static void SetRelative(DependencyObject o, double value)
{
o.SetValue(RelativeProperty, value);
}
and a converter:
public class RelativePositionConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var rel = (double)values[0];
var width = (double)values[1];
return rel * width;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
and when you add child to Canvas you need like this:
var child = new Child();
SetRelative(child, currentPosition / ActualWidth);
var multiBinding = new MultiBinding { Converter = new RelativePositionConverter() };
multiBinding.Bindings.Add(new Binding { Source = child, Path = new PropertyPath(RelativeProperty) });
multiBinding.Bindings.Add(new Binding { Source = canvas, Path = new PropertyPath(ActualWidthProperty) });
BindingOperations.SetBinding(child, LeftProperty, multiBinding);
Children.Add(child);
If you need, you can change Relative value of child separate of Canvas
Related
I have created it using winforms by just adding permon classes and downloading some realtime dasboards but i find it quite difficult in WPF
There's a lot of tools that allows to draw various graphs in WPF.
But since i didn't find any manual graph drawing implementation, I've created the example - how to draw a graph in WPF using MVVM programming pattern.
0. Helper classes
For proper and easy MVVM implementation I will use 2 following classes.
NotifyPropertyChanged.cs - to notify UI on changes.
public class NotifyPropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
RelayCommand.cs - for easy commands use (for Button)
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Predicate<object> _canExecute;
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
=> (_execute, _canExecute) = (execute, canExecute);
public bool CanExecute(object parameter)
=> _canExecute == null || _canExecute(parameter);
public void Execute(object parameter)
=> _execute(parameter);
}
1. Data implementation
As graph consists of contant amount of points an they're just turning around, I've implemented the Round-Robin Collection that has only one method Push().
RoundRobinCollection.cs
public class RoundRobinCollection : NotifyPropertyChanged
{
private readonly List<float> _values;
public IReadOnlyList<float> Values => _values;
public RoundRobinCollection(int amount)
{
_values = new List<float>();
for (int i = 0; i < amount; i++)
_values.Add(0F);
}
public void Push(float value)
{
_values.RemoveAt(0);
_values.Add(value);
OnPropertyChanged(nameof(Values));
}
}
2. Values collection to Polygon points Converter
Used in View markup
PolygonConverter.cs
public class PolygonConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
PointCollection points = new PointCollection();
if (values.Length == 3 && values[0] is IReadOnlyList<float> dataPoints && values[1] is double width && values[2] is double height)
{
points.Add(new Point(0, height));
points.Add(new Point(width, height));
double step = width / (dataPoints.Count - 1);
double position = width;
for (int i = dataPoints.Count - 1; i >= 0; i--)
{
points.Add(new Point(position, height - height * dataPoints[i] / 100));
position -= step;
}
}
return points;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => null;
}
3. View Model
The main class containing all business logic of the App
public class MainViewModel : NotifyPropertyChanged
{
private bool _graphEnabled;
private float _lastCpuValue;
private ICommand _enableCommand;
public RoundRobinCollection ProcessorTime { get; }
public string ButtonText => GraphEnabled ? "Stop" : "Start";
public bool GraphEnabled
{
get => _graphEnabled;
set
{
if (value != _graphEnabled)
{
_graphEnabled = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ButtonText));
if (value)
ReadCpu();
}
}
}
public float LastCpuValue
{
get => _lastCpuValue;
set
{
_lastCpuValue = value;
OnPropertyChanged();
}
}
public ICommand EnableCommand => _enableCommand ?? (_enableCommand = new RelayCommand(parameter =>
{
GraphEnabled = !GraphEnabled;
}));
private async void ReadCpu()
{
try
{
using (PerformanceCounter cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total"))
{
while (GraphEnabled)
{
LastCpuValue = cpuCounter.NextValue();
ProcessorTime.Push(LastCpuValue);
await Task.Delay(1000);
}
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
public MainViewModel()
{
ProcessorTime = new RoundRobinCollection(100);
}
}
Disclamer: async void is not recommended approach to make something async but here the usage is safe because any possible Exception will be handled inside. For more information about why async void is bad, refer to the documentation - Asynchronous Programming.
4. View
The whole UI markup of the app
<Window x:Class="CpuUsageExample.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:CpuUsageExample"
mc:Ignorable="d"
Title="MainWindow" Height="400" Width="800" >
<Window.DataContext>
<local:MainViewModel/><!-- attach View Model -->
</Window.DataContext>
<Window.Resources>
<local:PolygonConverter x:Key="PolygonConverter"/><!-- attach Converter -->
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<WrapPanel>
<Button Margin="5" Padding="10,0" Content="{Binding ButtonText}" Command="{Binding EnableCommand}"/>
<TextBlock Margin="5" Text="CPU:"/>
<TextBlock Margin="0, 5" Text="{Binding LastCpuValue, StringFormat=##0.##}" FontWeight="Bold"/>
<TextBlock Margin="0, 5" Text="%" FontWeight="Bold"/>
</WrapPanel>
<Border Margin="5" Grid.Row="1" BorderThickness="1" BorderBrush="Gray" SnapsToDevicePixels="True">
<Canvas ClipToBounds="True">
<Polygon Stroke="CadetBlue" Fill="AliceBlue">
<Polygon.Resources>
<Style TargetType="Polygon">
<Setter Property="Points">
<Setter.Value>
<MultiBinding Converter="{StaticResource PolygonConverter}">
<Binding Path="ProcessorTime.Values"/>
<Binding Path="ActualWidth" RelativeSource="{RelativeSource AncestorType=Canvas}"/>
<Binding Path="ActualHeight" RelativeSource="{RelativeSource AncestorType=Canvas}"/>
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</Polygon.Resources>
</Polygon>
</Canvas>
</Border>
</Grid>
</Window>
P.S. There's no code-behind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
I am trying to bind the width and height of a Rect in a ViewPort like this:
<VisualBrush.Viewport>
<Rect Width="{Binding Path=MyWidth}" Height="{Binding Path=MyHeight}"/>
</VisualBrush.Viewport>
My binding works fine elsewhere but here I get the following error message:
A 'Binding' cannot be set on the 'Width' property of type 'Rect'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.
Edit I understand the error message. My question is how to work around it. How do I bind the height and width of the rect?
Use a MultiBinding like this:
<VisualBrush.Viewport>
<MultiBinding>
<MultiBinding.Converter>
<local:RectConverter/>
</MultiBinding.Converter>
<Binding Path="MyWidth"/>
<Binding Path="MyHeight"/>
</MultiBinding>
</VisualBrush.Viewport>
with a multi-value converter like this:
public class RectConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return new Rect(0d, 0d, (double)values[0], (double)values[1]);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
Updated Answer:
Rect is an struct and Height and Width are not dependency properties(see screen shot), so they can't be bound to anything.
Here is a way to do it using Dependency Property and Binding.
MyRect Class with Dependency Properties:
public class MyRect : DependencyObject,INotifyPropertyChanged
{
public MyRect()
{
this.Rect = new Rect(0d, 0d, (double)Width, (double)Height);
}
private Rect rect;
public Rect Rect
{
get { return rect; }
set
{
rect = value;
RaiseChange("Rect");
}
}
public double Height
{
get { return (double)GetValue(HeightProperty); }
set { SetValue(HeightProperty, value); }
}
// Using a DependencyProperty as the backing store for Height. This enables animation, styling, binding, etc...
public static readonly DependencyProperty HeightProperty =
DependencyProperty.Register("Height", typeof(double), typeof(MyRect), new UIPropertyMetadata(1d, OnHeightChanged));
public static void OnHeightChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue != null)
{
var MyRect = (dp as MyRect);
var hight = Convert.ToDouble(e.NewValue);
MyRect.Rect = new Rect(0d, 0d, MyRect.Rect.Width, hight);
}
}
public double Width
{
get { return (double)GetValue(WidthProperty); }
set { SetValue(WidthProperty, value); }
}
// Using a DependencyProperty as the backing store for Width. This enables animation, styling, binding, etc...
public static readonly DependencyProperty WidthProperty =
DependencyProperty.Register("Width", typeof(double), typeof(MyRect), new UIPropertyMetadata(1d, OnWidthChanged));
public static void OnWidthChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue != null)
{
var MyRect = (dp as MyRect);
var width = Convert.ToDouble(e.NewValue);
MyRect.Rect = new Rect(0d, 0d, width, MyRect.Rect.Height);
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaiseChange(string prop)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
}
View:
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
MyRect = new TabControl.MyRect();
}
private MyRect myRect;
public MyRect MyRect
{
get { return myRect; }
set { myRect = value; RaiseChange("MyRect");}
}
private void MySlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
if (MyRect != null)
{
MyRect.Height = e.NewValue/10;
MyRect.Width = e.NewValue/10;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaiseChange(string prop)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
}
XAML:
<Window x:Class="TabControl.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TabControl"
Title="MainWindow" Height="450" Width="525"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="80*"/>
<RowDefinition Height="80"/>
</Grid.RowDefinitions>
<Rectangle Name="recLogin" Height="300" Width="400" DataContext="{Binding MyRect}" >
<Rectangle.Fill>
<VisualBrush TileMode="None" Viewport="{Binding Rect}">
<VisualBrush.Visual>
<ScrollViewer Height="30" Width="100">
<Button Content="Transparent" Height="30" Width="80" />
</ScrollViewer>
</VisualBrush.Visual>
</VisualBrush>
</Rectangle.Fill>
</Rectangle>
<Slider Maximum="20" x:Name="MySlider" Value="4" TickFrequency="1" Grid.Row="1" TickPlacement="TopLeft" ValueChanged="MySlider_ValueChanged" />
</Grid>
Output:
I am trying to create a polar diagram using MVVM. The radial axis is wind speed and radial angles is wind direction. I can create the radial circles (ellipses) and the radial speed axis labels. For the angular lines (like wheel spokes) I created a custom control that was simply a line with a textblock at the end (giving the angular axis value). However, I can not get my program to set the properties of this custom control and therefore you cannot see them on the canvas (though they are there but the default length is zero and default text is null)?
OK, So my diagram.xaml is as follows:
<Grid>
<Grid.DataContext>
<local:DiagramViewModel x:Name="ViewModel" />
</Grid.DataContext>
<Viewbox x:Name="myView"
Stretch="Uniform"
StretchDirection="Both">
<ItemsControl ItemsSource="{Binding Points}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Height="400" Width="400"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Right" Value="{Binding Right}"/>
<Setter Property="Canvas.Left" Value="{Binding Left}"/>
<Setter Property="Canvas.Top" Value="{Binding Top}"/>
<Setter Property="Canvas.Bottom" Value="{Binding Bottom}"/>
<Setter Property="Canvas.ZIndex" Value="{Binding ZIndex}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type local:EllipseItem}">
<Ellipse Stroke="{Binding Stroke}"
StrokeThickness="{Binding StrokeThickness}"
StrokeDashArray="{Binding StrokeDashArray}"
Height="{Binding Height}"
Width="{Binding Width}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:GridLineItem}">
<View:GridLineComponent Text="{Binding Path=Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Length="{Binding Path=Length, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
GridHeight="{Binding Path=GridHeight, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
LineColour="{Binding Path=LineColour, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
TextColour="{Binding Path=TextColour, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
RenderTransform="{Binding RenderTransform}"
RenderTransformOrigin="{Binding RenderTransformOrigin}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:TextBlockItem}">
<TextBlock Text="{Binding Text}"
Foreground="{Binding Foreground}"
FontSize="{Binding FontSize}"/>
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
</Viewbox>
</Grid>
My viewmodel (DiagramViewModel), as well as other things, contains the following method to create these lines:
private void CreateAngularLines(double originMargin)
{
IList<GridLineItem> angularLine = new List<GridLineItem>();
double increment , angularGridlineCount, angle, height, width;
if (this.minorAngularIncrement == null)
increment = (double)this.majorAngularIncrement;
else
increment = (double)this.minorAngularIncrement;
angularGridlineCount = 360 / increment;
height = 200;//half the length of the canvas
width = 200;
for (int i = 0; i < angularGridlineCount; i++)
{
angularLine.Add(new GridLineItem());
angle = i * increment;
if (angle == 0 || angle == 180)
angularLine[i].Text = angle.ToString("000");
else if (angle < 180)
angularLine[i].Text = "G" + angle.ToString("000");
else
angularLine[i].Text = "R" + (360 - angle).ToString("000");
angularLine[i].Length = height - originMargin;
angularLine[i].GridHeight = height;
angularLine[i].LineColour = this.LineColour;
angularLine[i].TextColour = this.TextColour;
angularLine[i].Left = width - 18;
angularLine[i].Top = 0;
angularLine[i].Right = width;
angularLine[i].Bottom = height;
angularLine[i].ZIndex = 1;
angularLine[i].RenderTransformOrigin = new Point(0.5, 1);
angularLine[i].RenderTransform = new RotateTransform(angle, 0, 0);
}
Points.Add(new CollectionContainer() { Collection = angularLine });
}
The GridLineItem class is a simple holder class with the following:
public class GridLineItem
{
public GridLineItem()
{
this.RenderTransformOrigin = new Point(0.5, 1);
this.RenderTransform = new RotateTransform(0, 0, 0);
this.Left = double.NaN;
this.Right = double.NaN;
this.Top = double.NaN;
this.Bottom = double.NaN;
}
public Brush LineColour { get; set; }
public Brush TextColour { get; set; }
public string Text { get; set; }
public double Length { get; set; }
public double GridHeight { get; set; }
public Point RenderTransformOrigin { get; set; }
public Transform RenderTransform { get; set; }
public int ZIndex { get; set; }
public double Left { get; set; }
public double Right { get; set; }
public double Top { get; set; }
public double Bottom { get; set; }
}
Now this is where I think I am going wrong. The GridlineComponent.xaml.cs file contains the following:
public partial class GridLineComponent : UserControl, INotifyPropertyChanged
{
public GridLineComponent()
{
InitializeComponent();
this.DataContext = this;
}
public GridLineComponent(string text, double length, double gridHeight) : this()
{
this.Text = text;
this.Length = length;
this.GridHeight = gridHeight;
}
public Brush LineColour
{
get { return (Brush)GetValue(LineColourProperty); }
set
{
SetValue(LineColourProperty, value);
NotifyPropertyChanged("LineColour");
}
}
public Brush TextColour
{
get { return (Brush)GetValue(TextColourProperty); }
set
{
SetValue(TextColourProperty, value);
NotifyPropertyChanged("TextColour");
}
}
public string Text
{
get { return (string)GetValue(TextProperty); }
set
{
if (value == null)
SetValue(TextProperty, "");
else
SetValue(TextProperty, value); //"\u00B0";
NotifyPropertyChanged("Text");
}
}
public double Length
{
get { return (double)GetValue(LengthProperty); }
set
{
SetValue(LengthProperty, value);
NotifyPropertyChanged("Length");
}
}
public double GridHeight
{
get { return (double)GetValue(GridHeightProperty); }
set
{
SetValue(GridHeightProperty, value);
NotifyPropertyChanged("GridHeight");
}
}
public static readonly DependencyProperty TextColourProperty =
DependencyProperty.Register(
"TextColour",
typeof(Brush),
typeof(GridLineComponent),
new PropertyMetadata(default(Brush), OnItemsPropertyChanged));
public static readonly DependencyProperty LineColourProperty =
DependencyProperty.Register(
"LineColour",
typeof(Brush),
typeof(GridLineComponent),
new PropertyMetadata(default(Brush), OnItemsPropertyChanged));
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(
"Text",
typeof(string),
typeof(GridLineComponent),
new PropertyMetadata(default(string), OnItemsPropertyChanged));
public static readonly DependencyProperty LengthProperty =
DependencyProperty.Register(
"Length",
typeof(double),
typeof(GridLineComponent),
new PropertyMetadata(default(double), OnItemsPropertyChanged));
public static readonly DependencyProperty GridHeightProperty =
DependencyProperty.Register(
"GridHeight",
typeof(double),
typeof(GridLineComponent),
new PropertyMetadata(default(double), OnItemsPropertyChanged));
private static void OnItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
GridLineComponent source = d as GridLineComponent;
// Do something...
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
}
Basically this GridLineComponent is not following the rules of MVVM. But I think that is ok here as it is such a basic type. The xaml for it is simply a grid containing a line and a TextBlock. The line's Y2 and stroke attributes are binded to the xaml.cs (Length and LineColour respectively) and so too are the textblock's foreground and text attributes (TextColour and Text respectively).
From what I have read, these lines should draw but do not. I think it may have something to do with the dependency properties or because of the empty OnItemsPropertyChanged handler. ALso, when the program is run, only the non argument constructor, for the GridlineComponent.xaml.cs is accessed (non of the properties are set)?
I'd like to create my own UserControl (let's call it "RectAtCoordinates") that would work similarly to Canvas, however it should:
-Store collection of (x,y) integer coordinates
-Draw rectangle (with arbitrary chosen size and fill) on canvas for each (x,y) pair. (x,y) specify position of rectangle.
First of all, I've created simple Coordinates class:
class Coordinates : DependencyObject
{
public static readonly DependencyProperty XProperty =
DependencyProperty.Register("X", typeof(int), typeof(Coordinates));
public static readonly DependencyProperty YProperty =
DependencyProperty.Register("Y", typeof(int), typeof(Coordinates));
public int X
{
get { return (int)GetValue(XProperty); }
set { SetValue(XProperty, value); }
}
public int Y
{
get { return (int)GetValue(YProperty); }
set { SetValue(YProperty, value); }
}
public Coordinates(int x, int y)
{
this.X = x;
this.Y = y;
}
}
Here's my RectAtCoordinates UserControl (.xaml):
<UserControl x:Class="RectAtPoint.RectAtCoordinates"
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"
d:DesignHeight="300" d:DesignWidth="300">
<ItemsControl ItemsSource="{Binding Path=Coos, Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Width="300" Height="300" Background="White"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Rectangle Fill="Black" Width="50" Height="50"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Left" Value="{Binding Path=X}" />
<Setter Property="Canvas.Top" Value="{Binding Path=Y}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</UserControl>
And finally code behind RectAtCoordinates:
public partial class RectAtCoordinates : UserControl
{
public static readonly DependencyProperty CoosProperty =
DependencyProperty.Register("Coos", typeof(Coordinates), typeof(RectAtCoordinates));
private ObservableCollection<Coordinates> Coos
{
get { return (ObservableCollection<Coordinates>)GetValue(CoosProperty); }
set { SetValue(CoosProperty, value); }
}
public RectAtCoordinates()
{
InitializeComponent();
Coos = new ObservableCollection<Coordinates>();
}
public void AddRectangleAtPosition(int x, int y)
{
Coos.Add(new Coordinates(x, y));
}
}
However, after building my project it crashes. I get CLR20r3 problem. After further inspection I've changed RectAtCoordinates constructor into:
public RectAtCoordinates()
{
InitializeComponent();
try
{
Coos = new ObservableCollection<Coordinates>();
}
catch (Exception e)
{
MessageBox.Show(e.ToString());
}
}
And got this error:
System.ArgumentException:
'System.Collections.ObjectModel.ObservableCollection`1[RectAtPoint.Coordinates]'
is not a valid value for property 'Coos'.
at
System.Windows.DependencyObject.SetValueCommon(DependencyProperty dp,
Object value, PropertyMetadata metadata, Boolean
coerceWithDeferredReference, Boolean coerceWithCurrentValue,
OperationType operationType, Boolean isInternal)
at System.Windows.DependencyObject.SetValue(DependencyProperty dp,
Object value)
at RectAtPoint.RectAtCoordinates.set_Coos(ObservableCollection`1
value) in c:...\RectAtCoordinates.xaml.cs:line 28
at RectAtPoint.RectAtCoordinates..ctor() in
c:...\RectAtCoordinates.xaml.cs:line 36
I'm novice considering WPF, binding and Dependency Properties, so please, take into consideration that I could've made some basic mistakes. I've found many similar problems, but I couldn't fully understand solutions and therefore properly apply them.
I think that your problem is in here:
public static readonly DependencyProperty CoosProperty =
DependencyProperty.Register("Coos", typeof(Coordinates), typeof(RectAtCoordinates));
Is should be:
public static readonly DependencyProperty CoosProperty =
DependencyProperty.Register("Coos", typeof(ObservableCollection<Coordinates>), typeof(RectAtCoordinates));
Give it a try :)
=== EDIT === For the coordinates.
I think you can do something like:
public void AddRectangleAtPosition(int x, int y)
{
Coos.Add(new Coordinates(x, y));
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Coos"));
}
}
Then your class should have:
public partial class RectAtCoordinates : UserControl, INotifyPropertyChanged
And after that you can have a region like I usually have for the NotifyPropertyChanged as this:
#region notifypropertychanged region
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetField<T>(ref T field, T value, string propertyName)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
#endregion
}
Give a try :)
I've finally found a solution: data context was not set properly.
I had to add
this.DataContext = this;
to UserControl's constructor or add:
DataContext="{Binding RelativeSource={RelativeSource Self}}"
to UserControl's xaml definition.
As I'm developing my app, I'm finding that I'm re-creating a "tile" control far too often. Therefore I'm currently trying to move it into a User Control for re-use. However, it's currently not accepting any bindings that were previously working. So for example:
<Canvas Height="73" Width="73" VerticalAlignment="Top" Margin="10,10,8,0">
<Rectangle Height="73" Width="73" VerticalAlignment="Top" Fill="{Binding Path=Active, Converter={StaticResource IconBackground}}" />
<Image Height="61" Width="61" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="6" Source="{Binding Tone.Image}" />
</Canvas>
Works fine with the bindings,
<views:Tile Height="73" Width="73" Background="{Binding Path=Active, Converter={StaticResource IconBackground}, Mode=OneWay}" Icon="{Binding Path=Tone.Image, Mode=OneTime}" />
produces the error "the parameter is incorrect".
Here is the code for my Tile UserControl:
Tile.xaml
<UserControl x:Class="RSS_Alarm.Views.Tile"
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="100" d:DesignWidth="100">
<Grid x:Name="LayoutRoot">
<Canvas Height="100" Width="100" Margin="0,0,0,0">
<Rectangle Name="rectBackground" Height="100" Width="100" />
<Image Name="imgIcon" Height="80" Width="80" VerticalAlignment="Center" HorizontalAlignment="Center" Canvas.Left="10" Canvas.Top="10" />
</Canvas>
</Grid>
</UserControl>
Tile.xaml.cs
namespace RSS_Alarm.Views
{
public partial class Tile : UserControl
{
public Tile()
{
InitializeComponent();
}
public String Icon
{
get
{
return imgIcon.Source.ToString();
}
set
{
BitmapImage alarmIcon = new BitmapImage();
alarmIcon.UriSource = new Uri(value, UriKind.Relative);
imgIcon.Source = alarmIcon;
}
}
new public Brush Background
{
get
{
return rectBackground.Fill;
}
set
{
rectBackground.Fill = value;
}
}
new public double Height
{
get
{
return rectBackground.Height;
}
set
{
rectBackground.Height = value;
imgIcon.Height = value * 0.8;
}
}
new public double Width
{
get
{
return rectBackground.Width;
}
set
{
rectBackground.Width = value;
imgIcon.Width = value * 0.8;
}
}
}
}
If you need any more source, let me know and I'll post it. I don't have any problems when using a fixed value (Height and Width are fine, and if I set Background to Red then that also works fine), but changing to a Binding value throws the exception.
EDIT 1
Here's some updated code:
Tile.xaml.cs
#region Background
public static readonly DependencyProperty RectBackgroundProperty =
DependencyProperty.Register(
"RectBackground",
typeof(SolidColorBrush),
typeof(Tile),
new PropertyMetadata(new SolidColorBrush(Colors.Green), new PropertyChangedCallback(OnBackgroundChanged))
);
public static void OnBackgroundChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Debug.WriteLine("Filling background");
((Tile)d).rectBackground.Fill = (Brush)e.NewValue;
}
new public SolidColorBrush Background
{
get { return (SolidColorBrush)GetValue(RectBackgroundProperty); }
set {
Debug.WriteLine("Setting colour");
SetValue(RectBackgroundProperty, value);
}
}
#endregion
MainMenuControl.xaml.cs
// Class to determine the background colour of the icon (active/inactive)
public class IconBackground : System.Windows.Data.IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
bool b = (bool)value;
Debug.WriteLine("Converting colour. Value is " + b.ToString());
if (b)
{
return (Brush)App.Current.Resources["PhoneAccentBrush"];
}
else
{
return new SolidColorBrush(Colors.DarkGray);
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
SolidColorBrush brush = (SolidColorBrush)value;
if (brush.Color.Equals(Colors.DarkGray))
{
return false;
}
else
{
return true;
}
}
}
I'm also comparing the two methods side-by-side. The tile on the left is the defined Canvas with bindings fully working, while the tile on the right is the Tile UserControl, which only works with defined colours (Blue in this case)
In order to be able to bind in XAML it is not enough to make a property. You have to create a DependencyProperty.
The reason your Background binding works, is that UserControl itself has this property. If you set a breakpoint in your Background property setter, you will see that it is never called.
Here is an example of a DependencyProperty for your Background (not tested)
#region Background
public const string BackgroundPropertyName = "Background";
public new Brush Background
{
get { return (Background)GetValue (BackgroundProperty); }
set { SetValue (Background, value); }
}
public static new readonly DependencyProperty BackgroundProperty = DependencyProperty.Register (
BackgroundPropertyName,
typeof (Brush),
typeof (Tile),
new PropertyMetadata (BackgroundChanged));
static void BackgroundChanged (DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((Tile) d).rectBackground = (Brush)e.NewValue;
}
#endregion