I am using an ItemsControl to display a list of 1 - 10 items (usually 2 - 4). I am trying to satisfy all these requirements:
All rows must be the same height
All rows should be displayed at a height of 300 maximum if possible.
If there is not enough room to display all rows at 300 high, then display at the largest possible height.
If the largest possible height is less than 150, then display at maxsize and use a scrollbar
If the rows do not fill the page, then it must be vertically aligned at the top
This is what I have so far:
<Window x:Class="TestGridRows.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:vm="clr-namespace:TestGridRows"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance vm:MainViewModel}"
Height="570" Width="800">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Path=DataItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border MinHeight="150" MaxHeight="300" BorderBrush="DarkGray" BorderThickness="1" Margin="5">
<TextBlock Text="{Binding Path=TheNameToDisplay}" VerticalAlignment="Center" HorizontalAlignment="Center" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="1" IsItemsHost="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
</Window>
This is what it currently looks like with 1 item:
and this is what it should look like:
2 or 3 items display as expected:
For 4+ items, the scrollbar appears correctly but the items are all sized to 150, rather than 300:
Question
How do I align the content to top when there is only 1 item? (without breaking the other functionality obviously)
Bonus question: How do I get the items to resize to maxheight instead of minheight when there are 4+ items?
During the WPF layout process, measuring and arranging will be done in order. In most cast, if an UIElement has variable size, it will return minimum required as result. But if any layout alignment has been set to Stretch, UIElement will take as possible as it can in that direction in arranging. In your case, UniFormGrid will always return 160(which is Border.MinHeight + Border.Margin.Top + Border.Margin.Bottom) * the count of items as desired height in measuring result(which will stored in DesiredSize.DesiredSize.Height). But it will take ItemsControl.ActualHeight as arranged height since it has Stretch VerticalAlignment. So, if UniFormGrid.DesiredSize.Height was less then ItemsControl.ActualHeight, UniFormGrid and any child has Stretch VerticalAlignment will be stretch in vertically, until it encountered its MaxHeight. This is why your 1 item test resulted in the center. If you change UniFormGrid.VerticalAlignment or Border.VerticalAlignment to Top, you will get a 160 height item in the top of ItemsContorl.
The most simple solution to both questions is override the measuring result base on maximum row height and minimum row height. I write the codes in below and had done some basic tests, it seems to work just fine.
namespace WpfApp1
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
public class MyScrollViewer : ScrollViewer
{
public double DesiredViewportHeight;
public MyScrollViewer() : base() { }
protected override Size MeasureOverride(Size constraint)
{
// record viewport's height for late calculation
DesiredViewportHeight = constraint.Height;
var result = base.MeasureOverride(constraint);
// make sure that `ComputedVerticalScrollBarVisibility` will get correct value
if (ComputedVerticalScrollBarVisibility == Visibility.Visible && ExtentHeight <= ViewportHeight)
result = base.MeasureOverride(constraint);
return result;
}
}
public class MyUniformGrid : UniformGrid
{
private MyScrollViewer hostSV;
private ItemsControl hostIC;
public MyUniFormGrid() : base() { }
public double MaxRowHeight { get; set; }
public double MinRowHeight { get; set; }
protected override Size MeasureOverride(Size constraint)
{
if (hostSV == null)
{
hostSV = VisualTreeHelperEx.GetAncestor<MyScrollViewer>(this);
hostSV.SizeChanged += (s, e) =>
{
if (e.HeightChanged)
{
// need to redo layout pass after the height of host had changed.
this.InvalidateMeasure();
}
};
}
if (hostIC == null)
hostIC = VisualTreeHelperEx.GetAncestor<ItemsControl>(this);
var viewportHeight = hostSV.DesiredViewportHeight;
var rows = hostIC.Items.Count;
var rowHeight = viewportHeight / rows;
double desiredHeight = 0;
// calculate the correct height
if (rowHeight > MaxRowHeight || rowHeight < MinRowHeight)
desiredHeight = MaxRowHeight * rows;
else
desiredHeight = viewportHeight;
var result = base.MeasureOverride(constraint);
return new Size(result.Width, desiredHeight);
}
}
public class VisualTreeHelperEx
{
public static T GetAncestor<T>(DependencyObject reference, int level = 1) where T : DependencyObject
{
if (level < 1)
throw new ArgumentOutOfRangeException(nameof(level));
return GetAncestorInternal<T>(reference, level);
}
private static T GetAncestorInternal<T>(DependencyObject reference, int level) where T : DependencyObject
{
var parent = VisualTreeHelper.GetParent(reference);
if (parent == null)
return null;
if (parent is T && --level == 0)
return (T)parent;
return GetAncestorInternal<T>(parent, level);
}
}
}
Xaml
<Window x:Class="WpfApp1.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:WpfApp1"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
Height="570" Width="800">
<local:MyScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl>
<sys:String>aaa</sys:String>
<sys:String>aaa</sys:String>
<sys:String>aaa</sys:String>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="DarkGray" BorderThickness="1" Margin="5">
<TextBlock Text="{Binding ActualHeight, RelativeSource={RelativeSource AncestorType=ContentPresenter}}"
VerticalAlignment="Center" HorizontalAlignment="Center" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<local:MyUniformGrid Columns="1" MinRowHeight="150" MaxRowHeight="300" VerticalAlignment="Top"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</local:MyScrollViewer>
</Window>
Related
I'm currently creating a board game in WPF. I'm creating my board in my PlayerViewModel so I have a nice 10x10 board. I also create pieces that the player starts with (4). Here is where I get stuck, because I'm not quite sure if it's possible to just put the player's pieces in my GamePiece.xaml the way I've done it since this is the "component" that creates the whole board for me. The circle/image is the component I want as a player's disc.
I bind this into my GameView.xaml, however the problem is that I want to have circles as my pieces in the game. Obviously here I'm just creating the whole board and so it's also creating the pieces (circles/photos) and I can't seem to manipulate this and decide how many I want to show on the board in the beginning. I have tried different ways, like put the pieces on specific coordinates and just having the color of the square it's taking up on the board change, but it doesn't look very nice.
First, you would create a ListView and configure it to place the items in rows (by using WrapPanel as ItemsPanel and setting the width and height for the ListView), and -as you mentioned- you can use BoardPiece UserControl as ItemTemplate.
Your GameView.xaml can be something similar to this
<UserControl
x:Class="GameView">
<StackPanel Orientation="Vertical">
<ListView
Width="600"
Height="500"
ItemsSource="{Binding BoardPieces}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<testingThings:BoardPiece Margin="0" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button
Width="100"
Margin="8"
Click="ButtonBase_OnClick"
Content="Fill All" />
</StackPanel>
</UserControl>
Then, Configure the ViewModel: create your items and store them in an ObservableCollection to bind them later with the ListView..
public class GameViewViewModel
{
public ObservableCollection<BoardPieceItem> BoardPieces { set; get; } =
new ObservableCollection<BoardPieceItem>();
public GameViewViewModel()
{
for (var i = 0; i < 10; i++)
for (var j = 0; j < 10; j++)
{
// random coloring at initialization, do it as you want..
BoardPieces.Add(new BoardPieceItem
{
Index = (i, j),
RectangleColor = "#00FF00", // green
EllipseColor = (i + j) % 2 == 0
? "#00FFFFFF" // transparent
: "#000000", // Black
});
}
}
}
Now, Bind the ViewModel with the View
public partial class GameView
{
public GameView()
{
InitializeComponent();
DataContext = new GameViewViewModel();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
if (sender is Button { DataContext: TestDialogVm vm })
{
foreach (var item in vm.BoardPieces)
{
item.EllipseColor = "#0000FF";
}
}
}
}
In BoardPiece.xaml there is no need to use Converters
<UserControl
x:Class="SharedModule.Views.TestLab.BoardPiece"
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:testLab="clr-namespace:SharedModule.Views.TestLab"
d:DataContext="{d:DesignInstance testLab:BoardPieceItem}"
MouseDown="UIElement_OnMouseDown"
mc:Ignorable="d">
<Grid>
<Rectangle
Width="45"
Height="45"
Fill="{Binding RectangleColor}"
Stroke="Black"
StrokeThickness="1.3" />
<Ellipse
Width="20"
Height="20"
Fill="{Binding EllipseColor}" />
</Grid>
</UserControl>
Finally, Configure the BoardPieceItem to make it possible to update the game board at runtime..
public class BoardPieceItem : INotifyPropertyChanged
{
public (int, int) Index { get; set; }
private string _rectangleColor;
public string RectangleColor
{
get => _rectangleColor;
set
{
_rectangleColor = value;
OnPropertyChanged();
}
}
private string _ellipseColor;
public string EllipseColor
{
get => _ellipseColor;
set
{
_ellipseColor = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Now you are all set, see how UIElement_OnMouseDown will update the item's color, and ButtonBase_OnClick will update the cells of the board at all (I could go all the way MVVM, but used some events in a hurry).
This is how the game looks like at my side
I have a simple custom panel that is hosted within the ItemsPanel of an ItemsControl. The Template of the ItemsControl is updated to surround the ItemsPresenter with a Viewbox and a ScrollViewer. Here is the XAML code:
<ItemsControl ItemsSource="{Binding Buttons, RelativeSource={RelativeSource
AncestorType={x:Type Local:MainWindow}}}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.CanContentScroll="True">
<ItemsControl.Template>
<ControlTemplate TargetType="{x:Type ItemsControl}">
<ScrollViewer CanContentScroll="True">
<Viewbox Stretch="UniformToFill">
<ItemsPresenter />
</Viewbox>
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Local:TestPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Height="250" Width="250" HorizontalAlignment="Center"
Content="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
And the Panel:
public class TestPanel : Panel
{
protected override Size MeasureOverride(Size availableSize)
{
var desiredSize = new Size();
var layoutSlotSize = availableSize;
layoutSlotSize.Height = double.PositiveInfinity;
for (int i = 0, count = InternalChildren.Count; i < count; ++i)
{
UIElement child = InternalChildren[i];
if (child == null) continue;
child.Measure(layoutSlotSize);
var childDesiredSize = child.DesiredSize;
desiredSize.Width = Math.Max(desiredSize.Width, childDesiredSize.Width);
desiredSize.Height += childDesiredSize.Height;
}
return desiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
double verticalOffset = 0;
for (int i = 0, count = InternalChildren.Count; i < count; ++i)
{
UIElement child = InternalChildren[i];
if (child == null) continue;
child.Arrange(new Rect(0, verticalOffset, child.DesiredSize.Width,
child.DesiredSize.Height));
verticalOffset += child.DesiredSize.Height;
}
return base.ArrangeOverride(finalSize);
}
}
And finally, MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Buttons = new ObservableCollection<string>();
IEnumerable<int> characterCodes = Enumerable.Range(65, 26);
foreach (int characterCode in characterCodes)
Buttons.Add(((char)characterCode).ToString().ToUpper());
}
public static readonly DependencyProperty ButtonsProperty =
DependencyProperty.Register(nameof(Buttons), typeof(ObservableCollection<string>),
typeof(MainWindow), null);
public ObservableCollection<string> Buttons
{
get { return (ObservableCollection<string>)GetValue(ButtonsProperty); }
set { SetValue(ButtonsProperty, value); }
}
}
This all works as expected... so far, so good. The problem is when I change the base class from Panel to VirtualizingPanel, which I need to do to virtualise the data (not this example button data). After changing the base class, the panel immediately stops working. I am totally aware of how to virtualize data in a panel... I have a working example of this. My problem is when I want to put add a Viewbox inside the ScrollViewer.
Please note that this XAML will work fine with a normal Panel, or StackPanel, but as soon as I change it to VirtualizingPanel, it stops working (nothing is rendered, and the InternalChildren property contains no elements). Can anyone shed some light on this problem for me please?
I still do not know why the VirtualizingPanel does not work within a ViewBox within a ScrollViewer, but I have discovered that if I extend the VirtualizingStackPanel class in my panel instead, everything works as expected.
Therefore, the solution for those who require virtualized items to be stacked is to extend the VirtualizingStackPanel class instead. For those who need other types of child arrangement, I'm sorry, but I have no answer, unless you remove the Viewbox.
I would still be more than happy to receive any further information on this subject.
Is it possible to use a DataTemplate to render a collection of points as a bunch of lines (with data binding and drag and drop)?
Here are the details:
I have multiple objects in my view model. These objects ultimately have locations on a canvas specified in absolute pixel coordinates. I need to be able to drag and drop these items around on the canvas and update their coordinates. Some objects are represented by a point, others are a collection of line segments. I'm using MVVM (Jounce). Should my view model expose a ObservableCollection<Shape> that somehow binds the coordinates? That feels wrong. Or is there a way I can use DataTemplates here to draw lines with points on the end of each line segment given a collection of line segments? Here is an example ViewModel:
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Jounce.Core.ViewModel;
namespace CanvasBindTest.ViewModels
{
/// <summary>
/// Sample view model showing design-time resolution of data
/// </summary>
[ExportAsViewModel(typeof(MainViewModel))]
public class MainViewModel : BaseViewModel
{
public MainViewModel()
{
var start = new PointView { X = 0, Y = 0 };
var middle = new PointView { X = 1132 / 2, Y = 747 / 2 };
var end = new PointView() { X = 1132, Y = 747 };
var lineView = new LineView(new[] { start, middle, end });
Lines = new LinesView(new[] { lineView });
}
public LinesView Lines { get; private set; }
}
public class LinesView : BaseViewModel
{
public ObservableCollection<LineView> Lines { get; private set; }
public LinesView(IEnumerable<LineView> lines)
{
Lines = new ObservableCollection<LineView>(lines);
}
}
public class LineView : BaseViewModel
{
public ObservableCollection<PointView> Points { get; private set; }
public LineView(IEnumerable<PointView> points)
{
Points = new ObservableCollection<PointView>(points);
}
}
public class PointView : BaseViewModel
{
private int x, y;
public int X
{
get { return x; }
set { x = value; RaisePropertyChanged(() => X); }
}
public int Y {
get { return y; }
set { y = value; RaisePropertyChanged(() => Y); }
}
}
}
Here is the View, which is a canvas wrapped in a ItemsControl with a background image. The view model coordinates are relative to the background image's unscaled size:
<UserControl x:Class="CanvasBindTest.MainPage"
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:viewModels="clr-namespace:CanvasBindTest.ViewModels"
mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400">
<UserControl.Resources>
<DataTemplate x:Key="SkylineTemplate" DataType="viewModels:LineView">
<ItemsControl ItemsSource="{Binding Points}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<!--I have a collection of points here, how can I draw all the lines I need and keep the end-points of each line editable?-->
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
</UserControl.Resources>
<Grid d:DataContext="{d:DesignInstance viewModels:MainViewModel, IsDesignTimeCreatable=True}">
<ScrollViewer x:Name="Scroll">
<ItemsControl ItemsSource="{Binding Lines}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas>
<Canvas.Background>
<ImageBrush Stretch="Uniform" ImageSource="Properties/dv629047.jpg"/>
</Canvas.Background>
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
</Grid>
</UserControl>
Your LineView must be LineViewModel, it'll be more correct.
I describe the mechanism for points, for lines I think you will understand by yourself.
Main Control
Use ItemsControl.
ItemsControl.PanelControl must be Canvas.
ItemsSource - Your collection of PointWiewModel.
Make two DataTemplates for types PointWiewModel.
Make the PointView control and put it into the appropriate DataTemplate.
PointView control
Two way bind Canvas.X attached property to PointViewModel.X property.
Two way bind Canvas.Y attached property to PointViewModel.Y property.
Add logic of changing Canvas.X and Canvas.Y when you drag a PointView control.
Result
After that you could drag your (for example) PointVew control and the properties in your view model will be updated because of two way binding.
Suppose I understand correctly what do you want.
Added answers to the questions
Silverlight 5 supports it. That's mean all the items will be placed on the Canvas control. Some article about ItemsControl.
<ItemsControl>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas></Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
PointView is the second user control.
Note: I've described a way to draw an array of points with MVVM. You can drag each point on the canvas and receiving the new coordinates in your view model. (Maybe my description was a bit confusing on this stage so I've deleted LineViews from it)
In order to make a Lines, you have to connect your points. It'll be more difficult so I suggest you to make a variant with points only.
When you will be familiar with it, you can move your ItemsControl into templated control. Make your own ItemSource collection and drawing the Path by this points when they will change the position.
You can also search some opensource graph controls and see how they drawing curving lines by dots. Actually they usually do doing it with the Path like I have described.
Sorry, but I wouldn't write more because it'll became an article but not an answer)
P.S: It is interesting question, so If I have some free time I may be write an article. About templated controls you can read here.
It's absolutely disgusting how much XAML this takes. I'll look for a way to clean it up using styles and templates. Also, I need to draw the line to the center of the point, that shouldn't be hard. For now, below is what worked. I ended up created a Collection<Pair<Point, Point>> ViewModel to bind the "Line" collection. Otherwise I'm looking at the line point-by-point and can't draw a line since I can't find X2/Y2.
Thanks for the inspiration Alexander.
Here is the XAML:
<ItemsControl ItemsSource="{Binding Lines}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="viewModels:LineViewModel">
<ItemsControl ItemsSource="{Binding LineSegments}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ItemsControl>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl ItemsSource="{Binding Lines}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Line X1="{Binding Item1.X}" X2="{Binding Item2.X}" Y1="{Binding Item1.Y}" Y2="{Binding Item2.Y}" Stroke="Black" StrokeThickness="2"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl ItemsSource="{Binding LineSegment}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Canvas.Left="{Binding X}" Canvas.Top="{Binding Y}" Width="10" Height="10" Fill="Black">
<Ellipse.RenderTransform>
<TranslateTransform X="{Binding X}" Y="{Binding Y}"/>
</Ellipse.RenderTransform>
</Ellipse>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Here is the ViewModel:
namespace CanvasBindTest.ViewModels
{
/// <summary>
/// Sample view model showing design-time resolution of data
/// </summary>
[ExportAsViewModel(typeof (MainViewModel))]
public class MainViewModel : BaseViewModel
{
public MainViewModel()
{
var start = new PointViewModel {X = 0, Y = 0};
var middle = new PointViewModel {X = 30, Y = 10};
var end = new PointViewModel {X = 20, Y = 0};
var simpleLine = new LineSegmentsViewModel(new[] {start, middle, end});
Lines = new ObservableCollection<LineViewModel> {new LineViewModel(new[] {simpleLine})};
}
public ObservableCollection<LineViewModel> Lines { get; private set; }
}
public class LineViewModel : BaseViewModel
{
public LineViewModel(IEnumerable<LineSegmentsViewModel> lineSegments)
{
LineSegments = new ObservableCollection<LineSegmentsViewModel>(lineSegments);
}
public ObservableCollection<LineSegmentsViewModel> LineSegments { get; private set; }
}
public class LineSegmentsViewModel : BaseViewModel
{
public LineSegmentsViewModel(IEnumerable<PointViewModel> lineSegment)
{
LineSegment = new ObservableCollection<PointViewModel>(lineSegment);
Lines = new Collection<Tuple<PointViewModel, PointViewModel>>();
var tmp = lineSegment.ToArray();
for (var i = 0; i < tmp.Length - 1; i++)
{
Lines.Add(new Tuple<PointViewModel, PointViewModel>(tmp[i], tmp[i+1]));
}
}
public Collection<Tuple<PointViewModel, PointViewModel>> Lines { get; private set; }
public ObservableCollection<PointViewModel> LineSegment { get; private set; }
}
public class PointViewModel : BaseViewModel
{
private int x, y;
public int X
{
get { return x; }
set
{
x = value;
RaisePropertyChanged(() => X);
}
}
public int Y
{
get { return y; }
set
{
y = value;
RaisePropertyChanged(() => Y);
}
}
}
}
I'm trying to set up a WPF DataTemplate that will be used for Line (System.Windows.Shapes.Line) objects.
From a default .NET 4 WPF Application, I set my Window xaml to:
<Window x:Class="WpfTestDataTemplates.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate DataType="{x:Type system:String}" >
<TextBlock>It's a string</TextBlock>
</DataTemplate>
<DataTemplate DataType="{x:Type Line}" >
<TextBlock>It's a line</TextBlock>
</DataTemplate>
</Window.Resources>
<ListView ItemsSource="{Binding MyItems}" />
</Window>
And the code behind is:
using System.Collections.Generic;
using System.Windows;
using System.Windows.Shapes;
namespace WpfTestDataTemplates
{
public partial class MainWindow : Window
{
public List<object> MyItems {get; set; }
public MainWindow()
{
InitializeComponent();
DataContext = this;
MyItems = new List<object>();
MyItems.Add("The first string");
MyItems.Add(new Line { X1 = 0, Y1 = 0, X2 = 5, Y2 = 5 });
MyItems.Add("The second string");
MyItems.Add(new Rectangle { Height = 5, Width = 15 });
MyItems.Add(42);
}
}
}
The resulting window looks like this:
I expect the second entry to read: It's a line, but instead it seems the DataTemplate for the Line type is not found. For types without an explicit DataTemplate, I expect the default rendering to be the .ToString() member of the object, but that's not what is happening either. So I'd expect the fourth entry to read: System.Windows.Shapes.Rectangle
Why is the {x:Type Line} type not being recognized, and what DataTemplate is being applied to Shape objects?
A DataTemplate is what you use to put UI on data objects that aren't themselves UIElements and have no concept of rendering themselves on-screen. Line and Rectangle however are UIElements - they know how to render themselves, and don't need a DataTemplate to tell them how.
If you give your Line and Rectangle some color, you'll see that they disregard the well-meaning DataTemplate and show up in the list as a line and a rectangle:
MyItems.Add(new Line { X1 = 0, Y1 = 0, X2 = 5, Y2 = 5,
Stroke = Brushes.Lime, StrokeThickness = 2 });
...
MyItems.Add(new Rectangle { Height = 5, Width = 15, Fill = Brushes.Blue });
To change the appearance of UIElements, you'd typically use Style (if it is a FrameworkElement) and/or ControlTemplate (if it is a Control).
Edit:
If, instead of a Line, you have your own data class representing a line (let's call it LineData), you can use a DataTemplate to render that class any way you'd like:
public class LineData
{
public LineData(Point start, Point end)
{
this.Start = start;
this.End = end;
}
public Point Start { get; private set; }
public Point End { get; private set; }
public double XLength
{
get { return this.End.X - this.Start.X; }
}
public double YLength
{
get { return this.End.Y - this.Start.Y; }
}
}
...
MyItems.Add(new LineData(new Point(10, 10), new Point(60, 30)));
..and the DataTemplate..
<DataTemplate DataType="{x:Type vm:LineData}" >
<StackPanel Orientation="Horizontal" SnapsToDevicePixels="True" >
<TextBlock>It's a line:</TextBlock>
<Grid>
<Rectangle Stroke="Black" StrokeThickness="1"
Width="{Binding Path=XLength}" Height="{Binding Path=YLength}" />
<Line Stroke="Red" StrokeThickness="2"
X1="{Binding Path=Start.X}" Y1="{Binding Path=Start.Y}"
X2="{Binding Path=End.X}" Y2="{Binding Path=End.Y}" />
</Grid>
</StackPanel>
</DataTemplate>
..gives us:
I have a WPF ListView that should be extended with an always visible footer.
The footer shall behave like a header and should not be scrolled away.
The following XAML uses an external ScrollViewer linked to code behind to steer the ScrollViewer of the ListView:
<Window x:Class="LayoutTests.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="125" Width="176">
<Grid>
<StackPanel>
<ListView Name="L" ScrollViewer.HorizontalScrollBarVisibility="Hidden">
<ListViewItem Content="Brown brownie with a preference for white wheat."/>
<ListViewItem Content="Red Redish with a taste for oliv olives."/>
</ListView>
<ScrollViewer CanContentScroll="False" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" ScrollChanged="ScrollViewer_ScrollChanged">
<!-- Would like to bind Rectangle.Width to the preferred width of L -->
<Rectangle Height="20" Width="500" Fill="Red"/>
</ScrollViewer>
</StackPanel>
</Grid>
</Window>
In the code behind this looks like this:
private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
var bottomScrollViewer = sender as ScrollViewer;
var listScrollViewer = GetScrollViewer(L) as ScrollViewer;
if (listScrollViewer != null && bottomScrollViewer != null )
listScrollViewer.ScrollToHorizontalOffset( bottomScrollViewer.HorizontalOffset );
}
GetScrollViewer() is defined like this (but unimportant):
public static DependencyObject GetScrollViewer(DependencyObject depObj)
{
if (depObj is ScrollViewer)
{ return depObj; }
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = GetScrollViewer(child);
if (result == null) { continue; }
else { return result; }
}
return null;
}
The ScrollViewer of ListView obviously knows about the preferred width of its children.
The problem is that I cant find a way to bind to that width. So here it is:
How do I bind Rectangle.Width to the preferred size of the ListView?
Or, alternatively, how do I include a footer in the ListView that is always visible?
You need to bind against ExtentWidth of your ScrollViewer. According to http://msdn.microsoft.com/en-us/library/system.windows.controls.scrollviewer.extentwidth.aspx, it's a DependencyProperty. Mind that you need the ScrollViewer of your ListView, not the additional one you are creating below the list view.
You can use your GetScrollViewer function to find the ScrollViewer on the ListView. Of course, you'll need to set the binding in the code-behind. Something like that:
Binding b = new Binding("ExtentWidth") { Source = GetScrollViewer(L) };
rect.SetBinding(Rectangle.WidthProperty, b);