Positioning XAMLUI elements on ItemsControl with Canvas - c#

I'm building a Windows Universal application where I need to put some elements on a canvas. Since I want to do this MVVM style I've read that I should use an ItemsControl for it. Problem is that all the solutions I found are for WPF or Silverlight and don't work for Windows Universal. They appear on the canvas, but they keep positioning themselfs in the left upper corner.
XAML:
<ItemsControl ItemsSource="{Binding MyItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" FontSize="18"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding Left}" />
<Setter Property="Canvas.Top" Value="{Binding Top}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
XAML's code behind:
public MainPage()
{
this.InitializeComponent();
DataContext = new MyItemViewModel();
}
Viewmodel and model:
class MyItemViewModel
{
ObservableCollection<MyItem> _myItems;
public MyItemViewModel()
{
_myItems = new ObservableCollection<MyItem>();
_myItems.Add(new MyItem(10, 10, "First TextBlock"));
_myItems.Add(new MyItem(200, 400, "2nd TextBlock"));
_myItems.Add(new MyItem(400, 200, "The Third TextBlock"));
}
public ObservableCollection<MyItem> MyItems
{
get { return _myItems; }
set { _myItems = value; }
}
}
public class MyItem
{
public MyItem(double left, double top, string text)
{
Left = left;
Top = top;
Text = text;
}
public double Left { get; set; }
public double Top { get; set; }
public string Text { get; set; }
}

Use FrameworkElement as the TargetType instead:
<ItemsControl.ItemContainerStyle>
<Style TargetType="FrameworkElement">
<Setter Property="Canvas.Left" Value="{Binding Left}" />
<Setter Property="Canvas.Top" Value="{Binding Top}" />
</Style>
</ItemsControl.ItemContainerStyle>

Related

wpf mvvm - Is there a way to programmatically create textbox based on number of properties in model and bind to each of them in?

I have multiple objects with multiple properties which I want to display or hide (or create if needed) based on the property value.
For example:
Model:
public class InputModel
{
public string Name { get; set; }
public string Id { get; set; }
public string Type { get; set; }
public bool Required { get; set; }
public int Dpi { get; set; }
public string Data { get; set; }
public List<ColorModel> Filter { get; set; }
public List<RenderModel> Render { get; set; }
public List<LayoutModel> Layout { get; set; }
}
View:
<ItemsControl x:Name="InputsList"
Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Height="400">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel HorizontalAlignment="Center" Background="Beige" Margin="10">
<TextBlock Text="{Binding Name, StringFormat='Name: {0}'}"/>
<TextBlock Text="{Binding Id, StringFormat='Id: {0}'}"/>
<TextBlock Text="{Binding Type, StringFormat='Type: {0}'}"/>
<CheckBox Grid.Column="1" Content="Required" Margin="0,5,0,0"
IsChecked="{Binding ElementName=Required, Path=CheckBoxIsChecked}"/>
<TextBox Grid.Column="0" Grid.Row="4"
Text="{Binding Dpi, StringFormat='Dpi: {0}'}"/>
<TextBlock Grid.Column="0" Grid.Row="5"
Text="Render:"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
And my ViewModel:
public class InputsListViewModel : Screen
{
private IRootModel _rootModel;
public InputsListViewModel(IRootModel rootModel)
{
_rootModel = rootModel;
}
private BindingList<InputModel> _inputs;
public BindingList<InputModel> InputsList
{
get
{
_inputs = new (_rootModel.Inputs);
return _inputs;
}
set
{
_inputs = value;
NotifyOfPropertyChange(() => InputsList);
}
}
private BindingList<RenderModel> _renders;
public BindingList<RenderModel> RenderList
{
get
{
_renders = new(_rootModel.Render);
return _renders;
}
set
{
_renders = value;
NotifyOfPropertyChange(() => RenderList);
}
}
private BindingList<LayoutModel> _layouts;
public BindingList<LayoutModel> LayoutList
{
get
{
_layouts = new (_rootModel.Layout);
return _layouts;
}
set
{
_layouts = value;
NotifyOfPropertyChange(() => LayoutList);
}
}
}
I don't need to display all of the properties in here, just the ones that I need at a given time. And I want to be able to add any of the properties that are missing, if I need to.
I am a beginner and so far I only know how to manually create the fields that I need in view and bind to them.
Thanks!
you can hide whole items with ItemContainerStyle or individual Properties as shown in 2 different ways
<ItemsControl ItemsSource="{Binding MyItems}">
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Visibility" Value="{Binding ShowInUi, Converter={StaticResource BooleanToVisibilityConverter}}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name, StringFormat='Name: {0}'}"/>
<TextBlock Text="{Binding Id, StringFormat='Id: {0}'}"/>
<TextBlock Text="{Binding Type, StringFormat='Type: {0}'}"/>
<!--possibility 1-->
<TextBlock x:Name="AdditionalInfoText" Text="{Binding AdditionalInfo}"/>
<!--possibility 2-->
<TextBlock Text="{Binding SuperDuperValue}" Visibility="{Binding IsSuperDuperItem, Converter={StaticResource BooleanToVisibilityConverter}}"/>
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding AdditionalInfo}" Value="{x:Null}">
<Setter TargetName="AdditionalInfoText" Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
For all textblocks in an area
<Style TargetType="TextBlock">
<Style.Triggers>
<Trigger Property="Text" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed"/>
</Trigger>
<DataTrigger Binding="{Binding Text.Length, RelativeSource={RelativeSource Self}}" Value="0">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>

Hit-Testing in WPF for irregularly-shaped items

I have an irregularly shaped item (a line shape) contained within a ContentControl-derived class ("ShapeItem"). I style it with a custom cursor and I handle mouse-clicks within the ShapeItem class.
Unfortunately WPF thinks that the mouse is "over" my item if it is anywhere within the rectangular bounding box of the ContentControl. That's OK for closed shapes like a rect or circle, but it's a problem for a diagonal line. Consider this image with 3 such shapes on display and their bounding boxes shown in white:
Even if I am in the very bottom left hand corner of the bounding box around the line, it still shows the cursor and the mouse clicks still reach my custom item.
I want to change this so that that the mouse is only considered to be "over" the line detected if I am within a certain distance of it. Like, this region in red (forgive the crude drawing).
My question is, how do I approach this? Do I override some virtual "HitTest" related function on my ShapeItem?
I already know the math to figure out if I'm in the right place. I'm just wondering what approach is the best to choose. What functions do I override? Or what events do I handle, etc. I've gotten lost in the WPF documentation on Hit testing. Is it a matter of overriding HitTestCore or something like that?
Now for code. I host the items in a custom ItemsControl called "ShapesControl".
which uses the custom "ShapeItem" container to to host my view-model objects :
<Canvas x:Name="Scene" HorizontalAlignment="Left" VerticalAlignment="Top">
<gcs:ShapesControl x:Name="ShapesControl" Canvas.Left="0" Canvas.Top="0"
ItemsSource="{Binding Shapes}">
<gcs:ShapesControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="Transparent" IsItemsHost="True" />
</ItemsPanelTemplate>
</gcs:ShapesControl.ItemsPanel>
<gcs:ShapesControl.ItemTemplate>
<DataTemplate DataType="{x:Type gcs:ShapeVm}">
<Path ClipToBounds="False"
Data="{Binding RelativeGeometry}"
Fill="Transparent"/>
</DataTemplate>
</gcs:ShapesControl.ItemTemplate>
<!-- Style the "ShapeItem" container that the ShapesControl wraps each ShapeVm ine -->
<gcs:ShapesControl.ShapeItemStyle>
<Style TargetType="{x:Type gcs:ShapeItem}"
d:DataContext="{d:DesignInstance {x:Type gcs:ShapeVm}}"
>
<!-- Use a custom cursor -->
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Cursor" Value="SizeAll"/>
<Setter Property="Canvas.Left" Value="{Binding Path=Left, Mode=OneWay}"/>
<Setter Property="Canvas.Top" Value="{Binding Path=Top, Mode=OneWay}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type gcs:ShapeItem}">
<Grid SnapsToDevicePixels="True" Background="{TemplateBinding Panel.Background}">
<!-- First draw the item (i.e. the ShapeVm) -->
<ContentPresenter x:Name="PART_Shape"
Content="{TemplateBinding ContentControl.Content}"
ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}"
ContentTemplateSelector="{TemplateBinding ContentControl.ContentTemplateSelector}"
ContentStringFormat="{TemplateBinding ContentControl.ContentStringFormat}"
HorizontalAlignment="{TemplateBinding Control.HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding Control.VerticalContentAlignment}"
IsHitTestVisible="False"
SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}"
RenderTransformOrigin="{TemplateBinding ContentControl.RenderTransformOrigin}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</gcs:ShapesControl.ShapeItemStyle>
</gcs:ShapesControl>
</Canvas>
My "ShapesControl"
public class ShapesControl : ItemsControl
{
protected override bool IsItemItsOwnContainerOverride(object item)
{
return (item is ShapeItem);
}
protected override DependencyObject GetContainerForItemOverride()
{
// Each item we display is wrapped in our own container: ShapeItem
// This override is how we enable that.
// Make sure that the new item gets any ItemTemplate or
// ItemTemplateSelector that might have been set on this ShapesControl.
return new ShapeItem
{
ContentTemplate = this.ItemTemplate,
ContentTemplateSelector = this.ItemTemplateSelector,
};
}
}
And my "ShapeItem"
/// <summary>
/// A ShapeItem is a ContentControl wrapper used by the ShapesControl to
/// manage the underlying ShapeVm. It is like the the item types used by
/// other ItemControls, including ListBox, ItemsControls, etc.
/// </summary>
[TemplatePart(Name="PART_Shape", Type=typeof(ContentPresenter))]
public class ShapeItem : ContentControl
{
private ShapeVm Shape => DataContext as ShapeVm;
static ShapeItem()
{
DefaultStyleKeyProperty.OverrideMetadata
(typeof(ShapeItem),
new FrameworkPropertyMetadata(typeof(ShapeItem)));
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
// Toggle selection when the left mouse button is hit
base.OnMouseLeftButtonDown(e);
ShapeVm.IsSelected = !ShapeVm.IsSelected;
e.Handled = true;
}
internal ShapesControl ParentSelector =>
ItemsControl.ItemsControlFromItemContainer(this) as ShapesControl;
}
The "ShapeVm" is just an abstract base class for my view models. Roughly this:
public abstract class ShapeVm : BaseVm, IShape
{
public virtual Geometry RelativeGeometry { get; }
public bool IsSelected { get; set; }
public double Top { get; set; }
public double Left { get; set; }
public double Width { get; }
public double Height { get; }
}
You could use a ShapeItem class like shown below. It is a Canvas with two Path children, one for hit testing and one for display. It resembles a few of the typical Shape properties (which you may extend according to your needs).
public class ShapeItem : Canvas
{
public ShapeItem()
{
var path = new Path
{
Stroke = Brushes.Transparent,
Fill = Brushes.Transparent
};
path.SetBinding(Path.DataProperty,
new Binding(nameof(Data)) { Source = this });
path.SetBinding(Shape.StrokeThicknessProperty,
new Binding(nameof(HitTestStrokeThickness)) { Source = this });
Children.Add(path);
path = new Path();
path.SetBinding(Path.DataProperty,
new Binding(nameof(Data)) { Source = this });
path.SetBinding(Shape.FillProperty,
new Binding(nameof(Fill)) { Source = this });
path.SetBinding(Shape.StrokeProperty,
new Binding(nameof(Stroke)) { Source = this });
path.SetBinding(Shape.StrokeThicknessProperty,
new Binding(nameof(StrokeThickness)) { Source = this });
Children.Add(path);
}
public static readonly DependencyProperty DataProperty =
Path.DataProperty.AddOwner(typeof(ShapeItem));
public static readonly DependencyProperty FillProperty =
Shape.FillProperty.AddOwner(typeof(ShapeItem));
public static readonly DependencyProperty StrokeProperty =
Shape.StrokeProperty.AddOwner(typeof(ShapeItem));
public static readonly DependencyProperty StrokeThicknessProperty =
Shape.StrokeThicknessProperty.AddOwner(typeof(ShapeItem));
public static readonly DependencyProperty HitTestStrokeThicknessProperty =
DependencyProperty.Register(nameof(HitTestStrokeThickness), typeof(double), typeof(ShapeItem));
public Geometry Data
{
get => (Geometry)GetValue(DataProperty);
set => SetValue(DataProperty, value);
}
public Brush Fill
{
get => (Brush)GetValue(FillProperty);
set => SetValue(FillProperty, value);
}
public Brush Stroke
{
get => (Brush)GetValue(StrokeProperty);
set => SetValue(StrokeProperty, value);
}
public double StrokeThickness
{
get => (double)GetValue(StrokeThicknessProperty);
set => SetValue(StrokeThicknessProperty, value);
}
public double HitTestStrokeThickness
{
get => (double)GetValue(HitTestStrokeThicknessProperty);
set => SetValue(HitTestStrokeThicknessProperty, value);
}
}
public class ShapeItemsControl : ItemsControl
{
protected override DependencyObject GetContainerForItemOverride()
{
return new ShapeItem();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is ShapeItem;
}
}
You would use it an XAML like this:
<gcs:ShapeItemsControl ItemsSource="{Binding Shapes}">
<gcs:ShapeItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</gcs:ShapeItemsControl.ItemsPanel>
<gcs:ShapeItemsControl.ItemContainerStyle>
<Style TargetType="gcs:ShapeItem">
<Setter Property="Data" Value="{Binding RelativeGeometry}"/>
<Setter Property="Fill" Value="AliceBlue"/>
<Setter Property="Stroke" Value="Yellow"/>
<Setter Property="StrokeThickness" Value="3"/>
<Setter Property="HitTestStrokeThickness" Value="15"/>
<Setter Property="Cursor" Value="Hand"/>
</Style>
</gcs:ShapeItemsControl.ItemContainerStyle>
</gcs:ShapeItemsControl>
However, you may not need a ShapeItem class and a derived ItemsControl at all, when you put the Canvas in the ItemTemplate of a regular ItemsControl:
<ItemsControl ItemsSource="{Binding Shapes}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas Cursor="Hand">
<Path Data="{Binding RelativeGeometry}" Fill="Transparent"
Stroke="Transparent" StrokeThickness="15"/>
<Path Data="{Binding RelativeGeometry}" Fill="AliceBlue"
Stroke="Yellow" StrokeThickness="3"/>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
If you also need to support selection, you should use a ListBox instead of an ItemsControl. A third Path in the ItemTemplate could visualize the selection state.
<ListBox ItemsSource="{Binding Shapes}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Template>
<ControlTemplate TargetType="ListBox">
<ItemsPresenter/>
</ControlTemplate>
</ListBox.Template>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Canvas Cursor="Hand">
<Path Data="{Binding RelativeGeometry}" Fill="Transparent"
Stroke="Transparent" StrokeThickness="15"/>
<Path Data="{Binding RelativeGeometry}"
Stroke="Green" StrokeThickness="7"
StrokeStartLineCap="Square" StrokeEndLineCap="Square"
Visibility="{Binding IsSelected,
RelativeSource={RelativeSource AncestorType=ListBoxItem},
Converter={StaticResource BooleanToVisibilityConverter}}"/>
<Path Data="{Binding RelativeGeometry}" Fill="AliceBlue"
Stroke="Yellow" StrokeThickness="3"/>
</Canvas>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

Data Binding to menu using multiple data types and being able to set the icon

Before you mark as duplicate, I have read the other SO entries, none seem to apply
I have two classes, one for menu item groups which will comprise my root menu items, and one for the sub items as such
public class RootItem
{
public string name {get;set;}
public image icon {get;set;}
public List<SubItem> subItems {get;set;}
}
public class SubItem
{
public string name {get;set;}
public image icon {get;set;}
}
The rest of what I am trying to do is probibly obvious but ill outline it anyway:
<MenuItem ItemsSource="{Binding Path=RootItems}" Header="Menu">
<MenuItem.Resources>
<DataTemplate DataType="{x:Type root:RootItem}">
<DataTemplate.Resources>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Header" Value="{Binding name}"/>
<Setter Property="ItemsSource" Value="{Binding subItems}"/>
<Setter Property="Icon" Value="{Binding icon}"/>
</Style>
</DataTemplate.Resources>
</DataTemplate>
<DataTemplate DataType="{x:Type root:SubItem}">
<DataTemplate.Resources>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Header" Value="{Binding name}"/>
<Setter Property="Icon" Value="{Binding icon}"/>
</Style>
</DataTemplate.Resources>
</DataTemplate>
</MenuItem.Resources>
</MenuItem>
This is just one of the methods I tried, in this one it doesn't seem like the style is used at all as only the root items show but they are blank, no icon or name.
I must be able to use 2 separate classes and the icon must be set-able and there must be 2 data templates or styles to handle/show the 2 classes differently.
I have also tried this, which while a mess, gets me 90% of the way, I just have issues where the icon is only visible on the last menu item.
<MenuItem ItemsSource="{Binding Path=RootItems}" Header="Menu">
<MenuItem.Resources>
<imico:InputMapperIcon x:Key="ico" Icon="{Binding icon}" x:Shared="false"/>
<HierarchicalDataTemplate DataType="{x:Type root:RootItem}" x:Shared="false" ItemsSource="{Binding subItems}">
<HierarchicalDataTemplate.ItemContainerStyle>
<Style>
<Setter Property="MenuItem.Header" Value="{Binding name}"/>
<Setter Property="MenuItem.Icon" Value="{StaticResource ico}"/>
<Setter Property="MenuItem.Tag" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Menu}}, Path=DataContext}"/>
<EventSetter Event="MenuItem.Click" Handler="BtnAddCommand_Click"/>
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
</HierarchicalDataTemplate>
</MenuItem.Resources>
<MenuItem.ItemContainerStyle>
<Style>
<Setter Property="MenuItem.Header" Value="{Binding name}"/>
<Setter Property="MenuItem.Icon" Value="{StaticResource ico}"/>
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
here is a working example, copied from your second xaml snippet:
.xaml
<Grid Background="Transparent">
<Grid.ContextMenu>
<ContextMenu>
<MenuItem ItemsSource="{Binding Path=RootItems}" Header="Menu">
<MenuItem.Resources>
<HierarchicalDataTemplate DataType="{x:Type root:RootItem}" x:Shared="false" ItemsSource="{Binding subItems}">
<HierarchicalDataTemplate.ItemContainerStyle>
<Style>
<Setter Property="MenuItem.Header" Value="{Binding name}"/>
<Setter Property="MenuItem.Icon" Value="{Binding icon}"/>
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
</HierarchicalDataTemplate>
</MenuItem.Resources>
<MenuItem.ItemContainerStyle>
<Style>
<Setter Property="MenuItem.Header" Value="{Binding name}"/>
<Setter Property="MenuItem.Icon" Value="{Binding icon}"/>
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
</ContextMenu>
</Grid.ContextMenu>
</Grid>
.cs
public partial class MainWindow : Window
{
public ObservableCollection<RootItem> RootItems { get; set; }
public MainWindow()
{
InitializeComponent();
RootItems = new ObservableCollection<RootItem>();
var rootitem = new RootItem() { name = "rootitem" };
rootitem.subItems = new List<SubItem>();
var subitem = new SubItem() { name = "subitem" };
subitem.icon = new Image();
subitem.icon.Source = new BitmapImage(new Uri(#"C:\Users\username\source\repos\WpfApp3\WpfApp3\New Bitmap Image.bmp"));
var subitem2 = new SubItem() { name = "subitem2" };
subitem2.icon = new Image();
subitem2.icon.Source = new BitmapImage(new Uri(#"C:\Users\username\source\repos\WpfApp3\WpfApp3\New Bitmap Image.bmp"));
rootitem.subItems.Add(subitem);
rootitem.subItems.Add(subitem2);
RootItems.Add(rootitem);
this.DataContext = this;
}
}
public class RootItem
{
public string name { get; set; }
public Image icon { get; set; }
public List<SubItem> subItems { get; set; }
}
public class SubItem
{
public string name { get; set; }
public Image icon { get; set; }
}
so the mistake i imagine must be in how you assign image to your SubItem(which i removed in my example, i use a staticresource instead), or in your InputMapperIcon

WPF Listview Item Vertical Positioning with Canvas Overlap Issue

I have a word addin which has a WPF custom task pane. In that I have a Listview. I need to dynamicaly update the position of each listview item when word document scroll down. but some of them can be having same canvas.top(vertical Y) value.Then those items getting overlap.
I do not need to overlap those,I need as listview need to align one after another.
screen sample
Code Sample XAML..
<listViewTool:ListView x:Name="Results" Margin="0" BorderThickness="0" DockPanel.Dock="Top" Background="WhiteSmoke"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ItemsSource="{Binding Results}"
SelectionMode="Single">
<listViewTool:ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="Control.HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Control.Margin" Value="0"/>
<Setter Property="Control.Padding" Value="0"/>
<Setter Property="Control.BorderThickness" Value="0"/>
<Setter Property="IsSelected" Value="{Binding Path=Selected, Mode=TwoWay}"/>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
<Setter Property="Canvas.Left" Value="0"/>
<Setter Property="Canvas.Width" Value="{Binding RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type ListView}},Path=ActualWidth}" />
</Style>
</listViewTool:ListView.ItemContainerStyle>
<listViewTool:ListView.ItemsPanel>
<ItemsPanelTemplate>
<Canvas x:Name="CanvasMain" HorizontalAlignment="Stretch" Height="{Binding RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type ListView}},Path=ActuaHeight}"
ClipToBounds="True" />
</ItemsPanelTemplate>
</listViewTool:ListView.ItemsPanel>
</listViewTool:ListView>
Thanks.
I've made a simple solution, which looks like this:
Idea is to store height of items and recalculate position of all items when height of either is changed.
Item height is item template border ActualHeight. For each item: Y is initial position, Offset is calculated/used to bind (based on Height of items before current item).
Solution contains some debugging code (Text property and binding) and is not a pure MVVM (but you didn't ask for one):
xaml:
<ListView ItemsSource="{Binding Items}">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="Canvas.Left"
Value="0" />
<Setter Property="Canvas.Top"
Value="{Binding Offset}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListViewItem">
<Border CornerRadius="10"
BorderThickness="1"
BorderBrush="Gray"
SizeChanged="Border_SizeChanged">
<Expander Header="{Binding Text}">
<Grid Height="50" />
</Expander>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListView.ItemContainerStyle>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Height="{Binding ActualHeight, RelativeSource={RelativeSource FindAncestor, AncestorType=ListView}}"
ClipToBounds="True" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListView>
cs:
public class Item : INotifyPropertyChanged
{
// INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string property = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
public double Y { get; set; }
public double Height { get; set; }
double _offset;
public double Offset
{
get { return _offset; }
set
{
_offset = value;
OnPropertyChanged();
OnPropertyChanged(nameof(Text));
}
}
public string Text => $"Y={Y} Height={Height} Offset={Offset}";
}
public partial class MainWindow : Window
{
public ObservableCollection<Item> Items { get; } = new ObservableCollection<Item>();
public MainWindow()
{
InitializeComponent();
for (int i = 0; i < 10; i++)
Items.Add(new Item { Y = i * 40 });
DataContext = this;
}
void Border_SizeChanged(object sender, SizeChangedEventArgs e)
{
var border = (Border)sender;
var current = (Item)border.DataContext;
current.Height = border.ActualHeight;
// recalculate offset
var y = Items[0].Y;
foreach (var item in Items)
{
item.Offset = y > item.Y ? y : item.Y;
y = item.Offset + item.Height;
}
}
}

Path Position Incorrect When Using Bindings To Populate Paths Data Attribute

I have a hard coded Path shape inside of a Canvas. I want to have multiple shapes inside this canvas so I'm trying to shove the information of each shape into a class and then use an ItemsControl to render each one. When I use the ItemsControl, each shape is in a incorrect location (Too far up and left).
Displays Correctly
<Canvas>
<Path Style="{StaticResource OverlayPath}"
Height="87.934"
Width="96.067"
Canvas.Left="348.456"
Canvas.Top="204.525"
Data="M432.9,245.5 L428.26666,258.46667 439.86716,261.26627 443.46698,246.46662 443.06664,242.33348 437.06651,242.60046 428.26633,239.13402 429.9994,232.33489 424.66584,230.73536 423.86545,234.46865 414.39771,236.46845 413.86433,239.66813 409.99697,236.73509 403.8631,235.80185 402.66265,233.66874 405.86266,231.13566 404.39584,224.73631 407.06279,221.93696 407.19614,217.00454 402.52898,211.00525 401.46255,204.73933 389.99435,207.00605 387.06071,211.4055 387.32706,222.20415 377.85934,219.93777 355.4564,218.33797 354.38926,226.20365 348.38853,227.80345 348.52187,233.93602 351.18886,239.53532 C351.18886,239.53532 356.12278,238.6021 355.72274,238.6021 355.32269,238.6021 361.99016,251.80045 361.99016,251.80045 L366.79074,253.53357 366.39069,258.5996 369.05768,259.13287 367.32414,268.73167 368.57429,275.93113 371.64132,279.19775 374.44166,279.73103 374.57501,286.46394 387.57658,287.19684 387.84328,290.06317 394.64409,291.66265 396.64434,285.79638 394.77744,284.99648 396.24429,279.99709 398.17785,279.13053 396.17761,276.99746 398.91128,274.99771 406.64554,277.86402 417.78022,267.79859 417.91357,262.53257 414.12144,259.02467 425.4228,249.8258 420.92226,244.72642 423.52258,243.02663 428.92323,242.82666 z" />
</Canvas>
Displays Incorrectly
<Canvas>
<ItemsControl ItemsSource={Binding CanvasPaths}>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Path Style="{StaticResource OverlayPath}"
Data="{Binding Data}"
Height="{Binding Height}"
Width="{Binding Width}"
Canvas.Left="{Binding CanvasLeft}"
Canvas.Top="{Binding CanvasTop}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
Model
public class CanvasPath
{
public CanvasPath(string data, double height, double width, double canvasTop, double canvasLeft)
{
Data = data;
Height = height;
Width = width;
CanvasTop = canvasTop;
CanvasLeft = canvasLeft;
}
public string Data { get; set; }
public double Height { get; set; }
public double Width { get; set; }
public double CanvasTop { get; set; }
public double CanvasLeft { get; set; }
}
ViewModel
public class TestViewModel
{
private ObservableCollection<CanvasPath> test = new ObservableCollection<CanvasPath>()
{
new CanvasPath("M432.9,245.5 L428.26666,258.46667 439.86716,261.26627 443.46698,246.46662 443.06664,242.33348 437.06651,242.60046 428.26633,239.13402 429.9994,232.33489 424.66584,230.73536 423.86545,234.46865 414.39771,236.46845 413.86433,239.66813 409.99697,236.73509 403.8631,235.80185 402.66265,233.66874 405.86266,231.13566 404.39584,224.73631 407.06279,221.93696 407.19614,217.00454 402.52898,211.00525 401.46255,204.73933 389.99435,207.00605 387.06071,211.4055 387.32706,222.20415 377.85934,219.93777 355.4564,218.33797 354.38926,226.20365 348.38853,227.80345 348.52187,233.93602 351.18886,239.53532 C351.18886,239.53532 356.12278,238.6021 355.72274,238.6021 355.32269,238.6021 361.99016,251.80045 361.99016,251.80045 L366.79074,253.53357 366.39069,258.5996 369.05768,259.13287 367.32414,268.73167 368.57429,275.93113 371.64132,279.19775 374.44166,279.73103 374.57501,286.46394 387.57658,287.19684 387.84328,290.06317 394.64409,291.66265 396.64434,285.79638 394.77744,284.99648 396.24429,279.99709 398.17785,279.13053 396.17761,276.99746 398.91128,274.99771 406.64554,277.86402 417.78022,267.79859 417.91357,262.53257 414.12144,259.02467 425.4228,249.8258 420.92226,244.72642 423.52258,243.02663 428.92323,242.82666 z",
87.934, 96.067, 204.525, 348.456)
};
public ObservableCollection<CanvasPath> CanvasPaths
{
get
{
return test;
}
}
}
The Canvas.Left and Canvas.Top bindings in your ItemTemplate have no effect, because the Path control in the DataTemplate does not have a Canvas parent.
In order to make it work you would have to set the ItemsPanel and ItemContainerStyle properties like this:
<ItemsControl ItemsSource="{Binding CanvasPaths}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding CanvasLeft}"/>
<Setter Property="Canvas.Top" Value="{Binding CanvasTop}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Path Style="{StaticResource OverlayPath}"
Data="{Binding Data}"
Height="{Binding Height}"
Width="{Binding Width}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

Categories