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.
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 have a problem with IsSelected property. It doesn't send values from view to view-model. I posted my code below
ViewModel:
public class Viewmodel : INotifyPropertyChanged
{
private ObservableCollection<int> seznam;
public ObservableCollection<int> Seznam
{
get { return seznam; }
set
{
seznam = value;
}
}
public Viewmodel()
{
Seznam = new ObservableCollection<int>();
for (int i = 0; i < 3; i++)
{
Seznam.Add(i);
}
}
bool isSelected;
public bool IsSelected
{
get { return isSelected; }
set
{
isSelected = value;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
View:
<TreeView ItemsSource="{Binding Seznam}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
It still doesn't stop at breakpoint that I put on get { return isSelected; }
With your updated post, it is clear that you haven't implemented your view models correctly. In particular, your TreeView.ItemsSource is bound to the Seznam property of your only view model. This is a collection of int values.
This means that the data context for each item container in the TreeView, where you attempt to bind to the IsSelected property, is an int value. And of course, int values don't even have an IsSelected property.
(By the way, I am skeptical about your claim that "There are no binding errors". If you looked at the debug output, you certainly should have seen a binding error, where attempting to bind to the non-existent IsSelected property.)
And think about this for a moment: supposing the item container did manage to bind to the Viewmodel.IsSelected property. How many item containers do you think there are? And how many instances of Viewmodel do you think there are? You should believe that there are many item containers, i.e. one for each item in your collection. And that there is just one instance of Viewmodel. So, how would all those items' selection state even map to the single Viewmodel.IsSelected property?
The right way to do this would be to create a separate view model object for the collection, with a property for your int value, as well as properties for the IsSelected and IsExpanded states (since you originally had mentioned wanting both).
Here is the example I wrote earlier just to prove to myself that the usual approach would work as expected. You should not have any trouble adapting it to suit your needs…
Per-item view model:
class TreeItemViewModel : NotifyPropertyChangedBase
{
public ObservableCollection<TreeItemViewModel> Items { get; }
= new ObservableCollection<TreeItemViewModel>();
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set { _UpdateField(ref _isSelected, value, _OnBoolPropertyChanged); }
}
private bool _isExpanded;
public bool IsExpanded
{
get { return _isExpanded; }
set { _UpdateField(ref _isExpanded, value, _OnBoolPropertyChanged); }
}
private void _OnBoolPropertyChanged(bool obj)
{
_RaisePropertyChanged(nameof(FullText));
}
private string _text;
public string Text
{
get { return _text; }
set { _UpdateField(ref _text, value, _OnTextChanged); }
}
private void _OnTextChanged(string obj)
{
_RaisePropertyChanged(nameof(FullText));
}
public string FullText
{
get { return $"{Text} (IsSelected: {IsSelected}, IsExpanded: {IsExpanded})"; }
}
}
Main view model for window:
class MainViewModel : NotifyPropertyChangedBase
{
public ObservableCollection<TreeItemViewModel> Items { get; }
= new ObservableCollection<TreeItemViewModel>();
public ICommand ClearSelection { get; }
public MainViewModel()
{
ClearSelection = new ClearSelectionCommand(this);
}
class ClearSelectionCommand : ICommand
{
private readonly MainViewModel _parent;
public ClearSelectionCommand(MainViewModel parent)
{
_parent = parent;
}
#pragma warning disable 67
public event EventHandler CanExecuteChanged;
#pragma warning restore 67
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_parent._ClearSelection();
}
}
private void _ClearSelection()
{
_ClearSelection(Items);
}
private static void _ClearSelection(IEnumerable<TreeItemViewModel> collection)
{
foreach (TreeItemViewModel item in collection)
{
_ClearSelection(item.Items);
item.IsSelected = false;
item.IsExpanded = false;
}
}
}
XAML for window:
<Window x:Class="TestSO44513864TreeViewIsSelected.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:p="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:TestSO44513864TreeViewIsSelected"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<l:MainViewModel>
<l:MainViewModel.Items>
<l:TreeItemViewModel Text="One">
<l:TreeItemViewModel.Items>
<l:TreeItemViewModel Text="One A"/>
<l:TreeItemViewModel Text="One B"/>
</l:TreeItemViewModel.Items>
</l:TreeItemViewModel>
<l:TreeItemViewModel Text="Two"/>
<l:TreeItemViewModel Text="Three"/>
</l:MainViewModel.Items>
</l:MainViewModel>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Content="Clear Selection" Command="{Binding ClearSelection}"
HorizontalAlignment="Left"/>
<TreeView ItemsSource="{Binding Items}" Grid.Row="1">
<TreeView.ItemContainerStyle>
<p:Style TargetType="TreeViewItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</p:Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="l:TreeItemViewModel"
ItemsSource="{Binding Items}">
<TextBlock Text="{Binding FullText}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
</Window>
And for completeness…
The boilerplate base class for INotifyPropertyChanged implementation:
class NotifyPropertyChangedBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void _UpdateField<T>(ref T field, T newValue,
Action<T> onChangedCallback = null,
[CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, newValue))
{
return;
}
T oldValue = field;
field = newValue;
onChangedCallback?.Invoke(oldValue);
_RaisePropertyChanged(propertyName);
}
protected void _RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Instead of using the IsSelected on every node in the tree, use the TreeView.SelectedItem on the TreeView itself. From here you can bind, but the property is readonly.
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'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
we have a wpf-window with some textboxes and a datagrid.
the textboxes descripe a parent (class a) object and the datagrid lists a collection of "childs" (class b => not derived from class a).
the childs can inherit values from the parent.
for example if the parent (class a) has a property Foo then the child object (class b) has a property Nullable which can either override the value of the parent or inherit the value of the parent.
now the datagrid should display the value in gray (if it is inherited) or in black (if the user overrides the value in the grid cell).
Unfortunatly Binding to InheritedText doesnt work. Does someone have any idea?
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<UserControls:InheritedTextBoxControl
Text="{Binding Path=?}"
InheritedText="{Binding Path=?}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
Thanks in advance
Tobi
--UPDATE--
xaml of InheritedTextBoxControl:
<UserControl x:Class="Com.QueoMedia.CO2Simulationstool.WPF.Utils.UserControls.InheritedTextBoxControl"
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"
Width="Auto"
Height="Auto"
Name="cnt">
<Grid x:Name="LayoutRoot"
Background="White">
<TextBox TextChanged="TextBoxTextChanged"></TextBox>
<TextBlock Name="inheritedText"
IsHitTestVisible="False"
Margin="4,0"
VerticalAlignment="Center"
Opacity="0.5"
FontStyle="Italic"></TextBlock>
</Grid>
CodeBehind:
public partial class InheritedTextBoxControl : UserControl {
private bool _isInherited;
public static readonly DependencyProperty InheritedTextProperty = DependencyProperty.Register("InheritedText", typeof(String), typeof(InheritedTextBoxControl), new PropertyMetadata(""));
public static DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(InheritedTextBoxControl), new PropertyMetadata(default(string)));
public InheritedTextBoxControl() {
InitializeComponent();
}
public string InheritedText {
get { return (string)GetValue(InheritedTextProperty); }
set {
SetValue(InheritedTextProperty, value);
inheritedText.Text = value;
}
}
private bool IsInherited {
get { return _isInherited; }
set {
_isInherited = value;
if (value) {
inheritedText.Opacity = 0.5;
} else {
inheritedText.Opacity = 0;
}
}
}
public string Text {
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
private void TextBoxTextChanged(object sender, TextChangedEventArgs e) {
if (((TextBox)sender).Text.Length > 0) {
IsInherited = false;
} else {
IsInherited = true;
}
Text = ((TextBox)sender).Text;
}
}
The problem is the setter of your InheritedText property. WPF won't call this setter when the property is set from XAML. See Checklist for Defining a Dependency Property, section Implementing the "Wrapper" for details.
You will have to update inheritedText.Text in a PropertyChangedCallback like this:
public static readonly DependencyProperty InheritedTextProperty =
DependencyProperty.Register(
"InheritedText", typeof(string), typeof(InheritedTextBoxControl),
new PropertyMetadata(string.Empty, InheritedTextChanged));
private static void InheritedTextChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
((InheritedTextBoxControl)d).inheritedText.Text = (string)e.NewValue;
}
public string InheritedText
{
get { return (string)GetValue(InheritedTextProperty); }
set { SetValue(InheritedTextProperty, value); } // only call SetValue here
}
If someone is interested in the solution:
we did it using a CellTemplate containing a CustomControl name MaskedTextbox that has three properties (MaskedText, Text, IsMaskTextVisible) and a CellEditingTemplate to override the data.
The values are bound to an InheritableValueViewModel.
Tobi