Wpf ItemsControl with margin coordinates [duplicate] - wpf

I want to add a set of rectangles to the main window of my mvvm application. In my viewModel I've got a collection of objects which I convert to System.Windows.Shapes.Rectangle classes with a converter (code below):
ViewModel:
RecognizedValueViewModel
{
public ObservableCollection<BarcodeElement> BarcodeElements
{
get { return _BarcodeElements; }
set { _BarcodeElements = value; }
}
public RecognizedValueViewModel()
{
BarcodeElements = InitializeBarcodeElements();
}
}
Converter:
public BarcodeElementToRectangleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
Rectangle barcodeRectangle = GetRectangleFromBarcodeElement(value as BarcodeElement);
return barcodeRectangle;
}
}
The rectangles should be shown in a canvas in my MainWindow:
<Canvas x:Name="Canvas_Image_Main">
<!-- Show rectangles here -->
</Canvas>
I would add Rectangles to canvas in code but I don't now how many rectangles are there at runtime. Is there a way how I can achieve this? Tank you.

In a proper MVVM approach you would have a view model with an abstract representation of a list of rectangles, e.g. like this:
public class RectItem
{
public double X { get; set; }
public double Y { get; set; }
public double Width { get; set; }
public double Height { get; set; }
}
public class ViewModel
{
public ObservableCollection<RectItem> RectItems { get; set; }
}
Then you would have a view that uses an ItemsControl to visualize a collection of such Rect items. The ItemsControl would have a Canvas as its ItemsPanel and an appropriate ItemContainerStyle and ItemTemplate which each bind to the appropriate view model properties. It might look like this:
<ItemsControl ItemsSource="{Binding RectItems}">
<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>
<Rectangle Width="{Binding Width}" Height="{Binding Height}" Fill="Black"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
An alternative without Bindings in Style Setters (which don't work in UWP) might look like this:
<ItemsControl ItemsSource="{Binding RectItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Rectangle Width="{Binding Width}" Height="{Binding Height}" Fill="Black">
<Rectangle.RenderTransform>
<TranslateTransform X="{Binding X}" Y="{Binding Y}"/>
</Rectangle.RenderTransform>
</Rectangle>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

You can bind the collection of rectangles to an ItemControl and set its height, width and margin:
<ItemsControl ItemsSource="{Binding Path=RectangleCollection,Mode=TwoWay}">
<ItemsControl.ItemTemplate>
<DataTemplate >
<Canvas>
<Rectangle Stroke="Black" Heigth={some converter} Width={some converter} Margin={Some Converter}>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemControl>
Just an idea to get you started...

Related

How to Merge user controls that Binds to SAME obj smoothly?

I'm trying to merge some user controls that are binded to same target. At the start, it looks simple but I have no idea with this how can I deliver binding target to daughter control (controls inside merge control)?
I want to make this:
<Canvas>
<local:Teeth x:Name="sideR" Points="{Binding Points[0]}" IsClosedCurve="{Binding IsClosedCurve}"/>
<local:WrapTeeth Points="{Binding Points[0]}"/>
<ListBox ItemsSource="{Binding Points[0]}" ItemContainerStyle="{StaticResource PointListBoxItemStyle}">
<ListBox.Template>
<ControlTemplate>
<Canvas IsItemsHost="True"/>
</ControlTemplate>
</ListBox.Template>
</ListBox>
</Canvas>
into
<local:MergeControl Points="{Binding Points[0]}"/>
Your UserControl should have a Points dependency property like shown below. It is not clear from your question whether you need a more specialized collection type than IEnumerable. Possibly replace it with PointCollection or something more suitable.
public partial class MergeControl : UserControl
{
public static readonly DependencyProperty PointsProperty = DependencyProperty.Register(
"Points", typeof(IEnumerable), typeof(MergeControl));
public IEnumerable Points
{
get { return (IEnumerable)GetValue(PointsProperty); }
set { SetValue(PointsProperty, value); }
}
public MergeControl()
{
InitializeComponent();
}
}
The elements in the UserControl's XAML would bind to this property by RelativeSource Bindings. You may need to define another property for the IsClosedCurve Binding of the Teeth element.
<UserControl ...>
<UserControl.Resources>
<Style x:Key="PointListBoxItemStyle" TargetType="ListBoxItem">
...
</Style>
</UserControl.Resources>
<Canvas>
<local:Teeth x:Name="sideR"
Points="{Binding Points, RelativeSource={RelativeSource AncestorType=UserControl}}"
IsClosedCurve="{Binding IsClosedCurve, ...}"/>
<local:WrapTeeth
Points="{Binding Points, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
<ListBox
ItemsSource="{Binding Points, RelativeSource={RelativeSource AncestorType=UserControl}}"
ItemContainerStyle="{StaticResource PointListBoxItemStyle}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</Canvas>
</UserControl>
Note also that ItemsControls have an ItemsPanel property to set the Panel element that is used to contain their items.

MouseDragElementBehavior in DataTemplate

So here's an issue that I'm having. I'm trying to use MouseDragElementBehavior in listbox. I was able to make it work when I was creating items in listbox directly, as in this example:
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Items>
<Border Width="20" Height="20">
<i:Interaction.Behaviors>
<ei:MouseDragElementBehavior ConstrainToParentBounds="True"/>
</i:Interaction.Behaviors>
<Rectangle Fill="Red"/>
</Border>
</ItemsControl.Items>
</ItemsControl>
But as soon as I've started using DataTemplate, it stopped working.
<ItemsControl Grid.Column="1" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Items>
Test item
</ItemsControl.Items>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Width="20" Height="20">
<i:Interaction.Behaviors>
<ei:MouseDragElementBehavior ConstrainToParentBounds="True"/>
</i:Interaction.Behaviors>
<Rectangle Fill="Red"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Any ideas as to why? I can't really figure out how a DataTemplate would affect MouseDragElementBehavior.
The MouseDragElementBehavior acts upon the FrameworkElement you attach it to. In your case it is the Border element which will be contained by a ContentPresenter which is the container generated by the ItemsControl. You have set ConstrainToParentBounds="True" which will ensure the visual will not be displayed outside its container, in this case the ContentPresenter. There are a few options, some easy, one probably not worth undertaking (but I did to figure some stuff out).
Set ConstrainToParentBounds="False". I am supposing that you don't want the Border to leave the ItemsControl so this probably won't suit.
Set the ItemContainerStyle to a Style which sets the Template to and adds the interaction to a similarly configured ContentPresenter. The base implementation of the ItemsControl uses a vanilla ContentPresenter. A caveat here is that if you aren't using UI elements as items you will need to wrap the item in one using a custom items control (see this answer on setting the container style):
<ItemsControl Grid.Column="1" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<ContentPresenter>
<i:Interaction.Behaviors>
<ei:MouseDragElementBehavior ConstrainToParentBounds="True"/>
</i:Interaction.Behaviors>
</ContentPresenter>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style >
</ItemsControl.ItemContainerStyle>
<ItemsControl.Items>
Test item
</ItemsControl.Items>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Width="20" Height="20">
<Rectangle Fill="Red"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Attach the interaction using the ItemsControl.ItemContainerStyle. This is a little involved because the Interaction.Behaviors attached property only has a setter:
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="beh:AddCollectionsToSetter.Behaviors">
<Setter.Value>
<beh:BehaviorCollection>
<ei:MouseDragElementBehavior ConstrainToParentBounds="True"/>
</beh:BehaviorCollection>
</Setter.Value>
</Setter>
</Style>
</ItemsControl.ItemContainerStyle>
For this I had to create a separate attached property AddCollectionsToSetter.Behaviors which is read/write and a BehaviorCollection that allows the interactions to be added to.
public static class AddCollectionsToSetter
{
#region Behaviors Dependency Property (Attached)
/// <summary>Gets the behaviours to add.</summary>
public static BehaviorCollection GetBehaviors(DependencyObject obj)
{
return (BehaviorCollection)obj.GetValue(BehaviorsProperty);
}
/// <summary>Sets the behaviours to add.</summary>
public static void SetBehaviors(DependencyObject obj, BehaviorCollection value)
{
obj.SetValue(AddCollectionsToSetter.BehaviorsProperty, value);
}
/// <summary>DependencyProperty backing store for <see cref="Behaviors"/>. Represents the behaviours to add.</summary>
/// <remarks></remarks>
public static readonly DependencyProperty BehaviorsProperty =
DependencyProperty.RegisterAttached("Behaviors", typeof(BehaviorCollection), typeof(AddCollectionsToSetter), new PropertyMetadata(null, BehaviorsPropertyChanged));
private static void BehaviorsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var oldBehaviors = (BehaviorCollection)e.OldValue;
var newBehaviors = (BehaviorCollection)e.NewValue;
var interaction = Interaction.GetBehaviors(d);
interaction.RemoveRange(oldBehaviors); // extension method, simple iterate and remove
interaction.AddRange(newBehaviors.Clone()); // extension method, simple iterate and add
}
#endregion Behaviors Dependency Property (Attached)
}
public class BehaviorCollection : FreezableCollection<System.Windows.Interactivity.Behavior>
{
public BehaviorCollection()
: base()
{
}
public BehaviorCollection(int capacity)
: base(capacity)
{
}
public BehaviorCollection(IEnumerable<System.Windows.Interactivity.Behavior> behaviors)
: base(behaviors)
{
}
}

Events in Canvas works only for last element

I use ListView to display list of Buttons. While using StackPanel or WrapPanel as ItemsPanel of the ListView, everything works fine. When I try Canvas only the last one button from a collection is working, others doesn't even change their background when mouse is over them. Can I make all them work normally?
<ListView ItemsSource="{Binding collection1}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<Button Margin="{Binding margin}" Width="40" Height="40" Click=button_click />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
code behind:
public class object1
{
public double x0 { get; set; }
public double y0 { get; set; }
public Thickness margin { get { return new Thickness(x0, y0, 0, 0); } }
public object1(double x, double y)
{
x0= x;
y0 = y;
}
}
public MainWindow : Window
{
public ObservableCollection<object1> collection1 {get;set;}
public MainWindow()
{
InitializeComponent();
DataContext = this;
collection1=new ObservableCollection<object1>();
collection1.Add(new object1(20,20));
collection1.Add(new object1(20,80));
collection1.Add(new object1(80,20));
collection1.Add(new object1(80,80)); // ONLY THAT ONE WORKS
}
}
The problem is the way in which you do the positioning of the Buttons with a Margin inside the ListViewItem. All the ListViewItems lie on top of each other in the Canvas, and only the topmost gets input events.
Move the Margin binding out of the ItemTemplate into an ItemContainerStyle, so that it sets the Margin of a ListViewItem instead of a Button:
<ListView>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="Margin" Value="{Binding margin}"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<Button Width="40" Height="40" .../>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
When using a Canvas as ItemsPanel I'd strongly suggest to use the Canvas.Left and Canvas.Top properties for positioning item containers. You would usually bind them to properties of the data item type, like your x0 and y0:
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="Canvas.Left" Value="{Binding x0}"/>
<Setter Property="Canvas.Top" Value="{Binding y0}"/>
</Style>
</ListView.ItemContainerStyle>

Show tick image when item is selected in WPF Listbox

I have this simple ListBox which displays a list of images horizontal en vertical.
I have also added a tick image on every image. Now I would like to enable this tick image only when the item is selected in the Listbox.
How can I achieve this?
ListBox XAML:
<ListBox x:Name="PhotoCollection"
Grid.Row="2"
Grid.ColumnSpan="4"
ItemsSource="{Binding PhotoCollection, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
SelectionMode="Multiple">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<Border BorderBrush="White"
BorderThickness="2"
Margin="5"
Background="LightGray">
<Grid>
<Image Source="{Binding}"
Stretch="Uniform"
Width="50"
Height="50"
Margin="5" />
<Image Source="{StaticResource Check_24}"
Visibility="{Binding Converter={StaticResource VisibleConverter}, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}, AncestorLevel=1},Path=IsSelected}"
Stretch="Uniform"
Width="20"
Height="20"
Margin="5"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"/>
</Grid>
</Border>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True"
Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
EDIT: This line does the trick
Visibility="{Binding Converter={StaticResource VisibleConverter}, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}, AncestorLevel=1},Path=IsSelected}"
It should work when you bind the IsSelected property of the ListBoxItem to your property IsVisible. But it depends on how you have implemented your ViewModel and the properties.
<ListBox>
<!-- the rest of the XAML-Definition of your ListBox -->
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsVisible, Mode=TwoWay}"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
When I need to bind visibility of items to boolean values, I've been using a converter class:
public class BoolToVisibleOrHidden : IValueConverter {
public BoolToVisibleOrHidden() { }
public bool Collapse { get; set; }
public bool Reverse { get; set; }
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
bool bValue = (bool)value;
if (bValue != Reverse) {
return Visibility.Visible;
} else {
if (Collapse)
return Visibility.Collapsed;
else
return Visibility.Hidden;
}
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
Visibility visibility = (Visibility)value;
if (visibility == Visibility.Visible)
return !Reverse;
else
return Reverse;
}
}
After grabbing an instance of it in XAML like so :
<local:BoolToVisibleOrHidden x:Key="BoolToVisConverter" Collapse="True" />
I can bind visibility of items like this:
Visibility="{Binding Converter={StaticResource BoolToVisConverter}, Path=DataContext.PATHTOBOOLEAN}"

How to convert X/Y position to Canvas Left/Top properties when using ItemsControl

I am trying to use a Canvas to display objects that have "world" location (rather than "screen" location). The canvas is defined like this:
<Canvas Background="AliceBlue">
<ItemsControl Name="myItemsControl" ItemsSource="{Binding MyItems}">
<Image x:Name="myMapImage" Panel.ZIndex="-1" />
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas>
<TextBlock Canvas.Left="{Binding WorldX}" Canvas.Top="{Binding WorldY}"
Text="{Binding Text}"
Width="Auto" Height="Auto" Foreground="Red" />
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
MyItem is defined like this:
public class MyItem
{
public MyItem(double worldX, double worldY, string text)
{
WorldX = worldX;
WorldY = worldY;
Text = text;
}
public double WorldX { get; set; }
public double WorldY { get; set; }
public string Text { get; set; }
}
In addition, I have a method to convert between world and screen coordinates:
Point worldToScreen(double worldX, double worldY)
{
// Note that the conversion uses an internal m_mapData object
var size = m_mapData.WorldMax - m_mapData.WorldMin;
var left = ((worldX - m_currentMap.WorldMin.X) / size.X) * myMapImage.ActualWidth;
var top = ((worldY - m_currentMap.WorldMin.Y) / size.Y) * myMapImage.ActualHeight;
return new Point(left, top);
}
With the current implementation, the items are positioned in the wrong location, because their location is not converted to screen coordinates.
How can I apply the worldToScreen method on the MyItem objects before they are added to the canvas?
Edit:
I got a little confused whether I'm going in the right way, so I posted another question: How to use WPF to visualize a simple 2D world (map and elements)
There is a helpful and complete answer there also for this question
The main problem with the code you presented is that the Canvas.Left and Canvas.Top properties are relative to a Canvas that is in the DataTemplate for the ItemsControl. This keeps "resetting" the origin. Instead you can:
remove the Canvas from the DataTemplate
make the ItemsPanel for the ListBox a Canvas
position the ItemsPresenter that wraps the ItemsControl items with Canvas.Top and Canvas.Left
ensure that the Image and the Canvas have the same coordinates, or switch to using the `Canvas
Here is a complete XAML-only example of positioning ItemsControl items on a Canvas with an Image behind the Canvas:
<Grid>
<Image x:Name="image" Height="100" Width="Auto" Source="http://thecybershadow.net/misc/stackoverflow.png"/>
<ItemsControl Name="myItemsControl">
<ItemsControl.ItemsSource>
<PointCollection>
<Point X="10" Y="10"/>
<Point X="30" Y="30"/>
</PointCollection>
</ItemsControl.ItemsSource>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="Text" Foreground="Red"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}"/>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</Grid>
You can apply this conversion within a value converter in your binding. Value converters implement the IValueConverter interface (http://msdn.microsoft.com/en-us/library/system.windows.data.ivalueconverter.aspx). The problem is that your conversion requires both the X and Y component of your item. A simple solution to this would be to bind to MyItem, rather than MyItem.WorldX. You can achieve this by using "Path=.", if you then create the following value converter ...
public class CoordinateLeftConverter: IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
MyItem item = value as MyItem;
return worldToScreen(item.WorldX, item.WorldY).X;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
}
}
You can use it in your binding as follows:
<TextBlock Canvas.Left="{Binding Path=.,Converter={StaticResource CoordinateLeftConverter}" ... />
Where you create an instance of CoordinateLeftConverter in your page Resources:
<UserControl.Resources>
<CoordinateLeftConverter x:Key="CoordinateLeftConverter"/>
</UserControl.Resources>
You would then of course need to add another converter for the Canvas.Top property, or supply a ConverterParameter to switch between the X / Y property of the transformed Point.
However, a simpler solution might be to perform the conversion within your MyItem class, removing the need for a converter!
I had a similar problem when I was trying to bind the Canvas.Top property to a ViewModel's object that has a CanvasTop property, the Canvas.Top property would first get the value, but then it gets reset somehow and loses the binding expression. But I did a little work around from the code here. And since I'm using Silverlight, there's no ItemsContainerStyle property, so I used ItemsControl.Resources instead, so given the above example, my code looks something like this:
<Grid>
<Image x:Name="image" Height="100" Width="Auto" Source="http://thecybershadow.net/misc/stackoverflow.png"/>
<ItemsControl Name="myItemsControl">
<ItemsControl.Resources>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}"/>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
</Style>
</ItemsControl.Resources>
<ItemsControl.ItemsSource>
<PointCollection>
<Point X="10" Y="10"/>
<Point X="30" Y="30"/>
</PointCollection>
</ItemsControl.ItemsSource>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="Text" Foreground="Red"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

Resources