I have a list of orders and when the order status is Cancelled, I want to blink the text. So far, my code works. However, sometimes it will throws exception:
WinRT information: Cannot resolve TargetName lblOrderStatus
For some reason lblOrderStatus can be found. So, I want to use "FindAncestor", but FindAncestor doesn't exists in UWP. Is there any equivalent function to FindAncestor in uwp?
Here is my code:
<ItemsControl x:Name="Orders" Grid.Row="1" Background="Transparent">
...
...
...
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
...
...
...
<Viewbox Grid.Column="3" StretchDirection="DownOnly" HorizontalAlignment="Right">
<TextBlock x:Name="lblOrderStatus" Text="{Binding Path=OrderItemStatus, Mode=OneWay}" FontSize="18">
<TextBlock.Resources>
<Storyboard x:Name="sbBlinking">
<DoubleAnimation Storyboard.TargetProperty="(FrameworkElement.Opacity)"
Storyboard.TargetName="lblOrderStatus"
From="1" To="0" AutoReverse="True" Duration="0:0:0.5" RepeatBehavior="Forever" />
</Storyboard>
</TextBlock.Resources>
<interactive:Interaction.Behaviors>
<core:DataTriggerBehavior Binding="{Binding OrderItemStatus, Converter={StaticResource EnumToStringConverter}}" ComparisonCondition="Equal" Value="Cancelled">
<media:ControlStoryboardAction Storyboard="{StaticResource sbBlinking}" />
</core:DataTriggerBehavior>
</interactive:Interaction.Behaviors>
</TextBlock>
</Viewbox>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Considering all the solutions I've seen, I feel that using ElementName binding is the simplest workaround to UWP not having a RelativeSource AncestorType binding option.
Assuming you've got a Page with its DataContext set to a viewmodel with a command MyCommand, and you want each item in your list to execute it when its button is clicked:
<Page Name="thisPage">
...
<ListView ...>
<ListView.ItemTemplate>
<DataTemplate>
<Button Command="{Binding ElementName=thisPage, Path=DataContext.MyCommand}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Page>
My initial problem with this solution is that you can't extract the DataTemplate out as a resource to use it on multiple screens (or even dialog boxes); thisPage might not exist in each of those places, or it might not be appropriate to name the root element "thisPage".
But if you use a convention where you include a token UI element in every screen that uses that DataTemplate, and refer to it by a consistent name, it will work. By default this element's DataContext will be your ViewModel (assuming your root element does too)
<Rectangle Name="VmDcHelper" Visibility="Collapsed"/>
...then in your standalone resources XAML file you can write your DataTemplate like this:
<DataTemplate x:Key="MyDataTemplate">
<Button Command="{Binding ElementName=VmDcHelper, Path=DataContext.MyCommand}" />
</DataTemplate>
Then, on every page/screen/dialog that you use that template resource, just drop in a copy of that Rectangle (or whatever) and everything will bind correctly at run-time
This is clearly a hack solution, but after thinking about it some more, it doesn't feel like any more of a hack than using WPF's AncestorType in the first place (having to ensure that your ancestor type is always consistent in all the places you use your DataTemplate).
I'm converting an app from WPF to UWP and found this thread. It seems there are no good solutions on the web, so here is my attempt to 'solve' this problem via workaround.
NOTE: The following is UNTESTED in UWP (but works in WPF) as I'm part way through a large non-compiling port, but theoretically it should work...
1 Create a RelativeSourceBinding Attached Property
This class has two properties: AncestorType and Ancestor. When the AncestorType changes, we subscribe to FrameworkElement.Loaded (to handle parent changes) and find the visual parent of type and assign to the Ancestor attached property.
public class RelativeSourceBinding
{
public static readonly DependencyProperty AncestorTypeProperty = DependencyProperty.RegisterAttached("AncestorType", typeof(Type), typeof(RelativeSourceBinding), new PropertyMetadata(default(Type), OnAncestorTypeChanged));
public static void SetAncestorType(DependencyObject element, Type value)
{
element.SetValue(AncestorTypeProperty, value);
}
public static Type GetAncestorType(DependencyObject element)
{
return (Type)element.GetValue(AncestorTypeProperty);
}
private static void OnAncestorTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((FrameworkElement)d).Loaded -= OnFrameworkElementLoaded;
if (e.NewValue != null)
{
((FrameworkElement)d).Loaded += OnFrameworkElementLoaded;
OnFrameworkElementLoaded((FrameworkElement) d, null);
}
}
private static void OnFrameworkElementLoaded(object sender, RoutedEventArgs e)
{
var ancestorType = GetAncestorType((FrameworkElement) sender);
if (ancestorType != null)
{
var findAncestor = ((FrameworkElement) sender).FindVisualParent(ancestorType);
RelativeSourceBinding.SetAncestor(((FrameworkElement)sender), findAncestor);
}
else
{
RelativeSourceBinding.SetAncestor(((FrameworkElement)sender), null);
}
}
public static readonly DependencyProperty AncestorProperty = DependencyProperty.RegisterAttached("Ancestor", typeof(UIElement), typeof(RelativeSourceBinding), new PropertyMetadata(default(FrameworkElement)));
public static void SetAncestor(DependencyObject element, UIElement value)
{
element.SetValue(AncestorProperty, value);
}
public static UIElement GetAncestor(DependencyObject element)
{
return (UIElement)element.GetValue(AncestorProperty);
}
}
Where FindVisualParent is an extension method defined as
public static UIElement FindVisualParent(this UIElement element, Type type)
{
UIElement parent = element;
while (parent != null)
{
if (type.IsAssignableFrom(parent.GetType()))
{
return parent;
}
parent = VisualTreeHelper.GetParent(parent) as UIElement;
}
return null;
}
2 Apply the RelativeSourceBinding property in XAML
some BEFORE xaml in WPF would look like this
<Style x:Key="SomeStyle" TargetType="local:AClass">
<Style.Setters>
<Setter Property="SomeProperty" Value="{Binding Foo, RelativeSource={RelativeSource AncestorType=local:AnotherClass}}" />
</Style.Setters>
</Style>
and AFTER xaml
<Style x:Key="SomeStyle" TargetType="local:AClass">
<Style.Setters>
<Setter Property="apc:RelativeSourceBinding.AncestorType" Value="local:AnotherClass"/>
<Setter Property="Foreground" Value="{Binding Path=(apc:RelativeSourceBinding.Ancestor).Foo, RelativeSource={RelativeSource Self}}" />
</Style.Setters>
</Style>
It's a bit messy but in the case where you only have one RelativeSource FindAncestor type to find, it should work.
In XAML
You can try using RelativeSource, it provides a means to specify the source of a binding in terms of a relative relationship in the run-time object graph.
For example using TemplatedParent:
Height="{Binding RelativeSource={RelativeSource TemplatedParent},
Path=Parent.ActualHeight}
or
<Binding RelativeSource="{RelativeSource TemplatedParent}" ></Binding>
In code you try using the VisualTreeHelper.GetParent method.
https://msdn.microsoft.com/en-us/library/windows/apps/windows.ui.xaml.media.visualtreehelper.getparent
something like the following, here is an example of a utility function
internal static void FindChildren<T>(List<T> results, DependencyObject startNode)
where T : DependencyObject
{
int count = VisualTreeHelper.GetChildrenCount(startNode);
for (int i = 0; i < count; i++)
{
DependencyObject current = VisualTreeHelper.GetChild(startNode, i);
if ((current.GetType()).Equals(typeof(T)) || (current.GetType().GetTypeInfo().IsSubclassOf(typeof(T))))
{
T asType = (T)current;
results.Add(asType);
}
FindChildren<T>(results, current);
}
}
The following example shows code that checks for an element's parent
((StackPanel)LinePane.Parent).ActualWidth;
Also, here is a good blog post showing this class in action. http://blog.jerrynixon.com/2012/09/how-to-access-named-control-inside-xaml.html
Related
I have a GridView with a ColumnHeaderTemplate
The template contains a path with the name arrow:
<DataTemplate x:Key="HeaderTemplate">
<DockPanel>
<Path DockPanel.Dock="Right" Margin="5,0,5,0" x:Name="arrow" StrokeThickness="1" Fill="Gray" Data="M 5,5 L 10,10 L 15,5 L 5,5" SnapsToDevicePixels="True"/>
</DockPanel>
</DataTemplate>
The template is assigned in the view like this:
<GridView ColumnHeaderTemplate="{StaticResource HeaderTemplate}">
The GridView is inside a ListView that manages the events
GridViewColumnHeader.Click="ListView_ColumnHeaderClick"
private void ListView_ColumnHeaderClick(object sender, RoutedEventArgs e)
When the event is triggered I want to be able to find the arrow control.
According to my research I should use the Template.FindName method, but so far I have not been able to make this work.
I cant seem to find the correct objects to use with the function and so I never find the control I am looking for.
No, the FindName method you mean would apply to ControlTemplate, not DataTemplate.
How to: Find ControlTemplate-Generated Elements
For DataTemplate you have to iterate the children maually using VisualTreeHelper.
How to: Find DataTemplate-Generated Elements
I do not know how you have attached the column header event handler, so I assume this:
<ListView ItemsSource="{Binding YourItemsSource}">
<ListView.Resources>
<DataTemplate x:Key="HeaderTemplate">
<DockPanel>
<Path DockPanel.Dock="Right" Margin="5,0,5,0" x:Name="arrow" StrokeThickness="1" Fill="Gray" Data="M 5,5 L 10,10 L 15,5 L 5,5" SnapsToDevicePixels="True"/>
</DockPanel>
</DataTemplate>
<Style x:Key="HeaderContainerStyle" TargetType="{x:Type GridViewColumnHeader}" BasedOn="{StaticResource {x:Type GridViewColumnHeader}}">
<EventSetter Event="Click" Handler="ListView_ColumnHeaderClick"/>
</Style>
</ListView.Resources>
<ListView.View>
<GridView ColumnHeaderTemplate="{StaticResource HeaderTemplate}"
ColumnHeaderContainerStyle="{StaticResource HeaderContainerStyle}">
<!-- ...your column definitions. -->
</GridView>
</ListView.View>
</ListView>
You have to create a custom method to recursively traverse the visual tree of the the grid view column header that checks the type and the name of the child elements to get the right one.
public T GetChild<T>(DependencyObject dependencyObject, string name) where T : FrameworkElement
{
if (dependencyObject == null)
return null;
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(dependencyObject); i++)
{
var child = VisualTreeHelper.GetChild(dependencyObject, i);
if (child is T frameworkElement && frameworkElement.Name.Equals(name))
return frameworkElement;
var nextChild = GetChild<T>(child, name);
if (nextChild != null)
return nextChild;
}
return null;
}
Then in the event handler, you can pass the sender, which is the column header.
private void ListView_ColumnHeaderClick(object sender, RoutedEventArgs e)
{
var gridViewColumnHeader = (GridViewColumnHeader)sender;
var arrow = GetChild<Path>(gridViewColumnHeader, "arrow");
// ... do something with arrow.
return;
}
Although this solution works and is a legitimate and officially documented way to solve your issue, you should usually not have to traverse the visual tree this way. In most cases it is not necessary as a lot of issues can be solved more elegantly and easier using data binding.
What I am attempting to do is have a collection of items shown in a GridView control and have the size of these items change based on a command executed by a separate button.
For example, having a row of buttons across the top reading “Small”, “Medium” and “Large” and having the items in the GridView respond to the relevant command by displaying its items in the relevant state.
I have the gridview declared like so
<GridView ItemsSource="{Binding Squares}"
With Squares being an observable collection of Square objects that have a Title and a Fill property.
At first I went down the DataTemplateSelector route by declaring the following data templates in the Resources section of the page.
<DataTemplate x:Key="SquareSmallTemplate">
<Grid Height="100" Width="100">
<Rectangle Fill="{Binding Fill}"/>
<TextBlock Text="{Binding Title}"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="SquareMediumTemplate">
<Grid Height="150" Width="150">
<Rectangle Fill="{Binding Fill}"/>
<TextBlock Text="{Binding Title}"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="SquareLargeTemplate">
<Grid Height="200" Width="200">
<Rectangle Fill="{Binding Fill}"/>
<TextBlock Text="{Binding Title}"/>
</Grid>
</DataTemplate>
The idea being that the grid’s height and width properties are different for the relevant template. I declared the following data templates in the selector
public DataTemplate SmallTemplate { get; set; }
public DataTemplate MediumTemplate { get; set; }
public DataTemplate LargeTemplate { get; set; }
And in the SelecteTemplateCore method I just returned the relevant template
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
string value = item as string;
if (value != null)
{
if (value == "Small")
return SmallTemplate;
else if (value == "Medium")
return MediumTemplate;
else if (value == "Large")
return LargeTemplate;
return base.SelectTemplate(item, container);
}
else
{
return base.SelectTemplateCore(item, container);
}
}
However, with this method (and, by design of the DataTemplateSelector) the object being passed in is the item in the collection (the Square).
This is fine if I wanted each item to have a different appearance or something, but what I need is the template to change based on another property on the view model.
For this, I have the following
public string State {get; set;}
and this is set to “Small”, “Medium, or “Large based on a separate row of three buttons that execute a command that sets this property to the relevant value.
How do I relate the State property to changing to the relevant DataTemplate?
Another route I tried was to have a single Data template that used the VSM to animate the Height/Width properties in the relevant states. However I could not get the relevant animation to execute when the State changed.
Any help would be great, thanks
There are a few ways to do this, I'm not sure which would be best. In any case, you'll need 1) a trigger, and 2) the action to update the template. I am leaning towards using PropertyChangedTrigger along with an InvokeCommandAction.
<GridView x:Name="grid">
<i:Interaction.Triggers>
<ei:PropertyChangedTrigger Binding="{Binding State}">
<i:InvokeCommandAction Command="{Binding RelativeSource={RelativeSource AncestorType=UserControl},Path=UpdateTemplateCommand}" CommandParameter="{Binding State}" />
</ei:PropertyChangedTrigger>
</i:Interaction.Triggers>
<GridView>
(Here the "AncestorType" would just be the root of the view, so please change "UserControl" as needed.)
Then in the view, you would have an ICommand that updates the template:
UpdateTemplateCommand = new DelegateCommand(state => {
switch ((string)state)
{
default:
case "Small" : grid.ItemTemplate = "SquareSmallTemplate"; break;
case "Medium" : grid.ItemTemplate = "SquareMediumTemplate"; break;
case "Large" : grid.ItemTemplate = "SquareLargeTemplate"; break;
}
});
IDK ... after writing this out it seems a bit convoluted. Maybe you'd find it preferable to add a CurrentDataTemplate property to the view-model, and assign it by creating DataTemplates from strings using XamlReader.
I've been working on this problem for a stupid amount of time. It is time to ask for directions despite my inner man saying "don't do it."
I am coding in WPF C# using MVVM design pattern. We try to adhere strictly to the pattern and put nothing in the code behind unless there is no option or it is completely unreasonable to do so. Having said that, I am working with a Telerik RadTreeView. Here is a snippet of it in my XAML:
<telerik:RadTreeView IsExpandOnSingleClickEnabled="True" IsLineEnabled="True" Margin="5"
ItemsSource="{Binding ItemsView}"
SelectedItem="{Binding SelectedWidget, Mode=TwoWay}"
ItemTemplate="{StaticResource NodeTemplate}" />
Currently the tree is working properly so that if you highlight a tree item and click the OK button on the view, all is good. However, I need to also allow the user to double click on one of the tree items. This means I already have a command and method, protected override void OkAction(), in my view model with the needed logic. Telerik supplies a property called ItemDoubleClick that is supposed to supply functionality for the tree item double click. But I can't find anything to allow me to do this in the view model. In other words, how do I do the binding? We also have a behavior setup in our project for double clicking that I was told I could use, but I have no experience with behaviors. I'm still a little wet with WPF.
If it helps, here is a link to the documentation for Telerik: http://www.telerik.com/help/wpf/radtreeview-events-overview.html
I would appreciate any help or direction anyone can provide.
Try this out Stan:
<Grid.Resources>
<DataTemplate x:Key="WidgetTemplate">
<StackPanel Orientation="Horizontal">
<Image Source="/Resources/gear1.png" Margin="1" Stretch="None" />
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" Margin="6,0,0,0" />
</StackPanel>
</DataTemplate>
<HierarchicalDataTemplate x:Key="NodeTemplate" ItemsSource = "{Binding Children}" ItemTemplate="{StaticResource WidgetTemplate}">
<TextBlock Text="{Binding Name}"/>
</HierarchicalDataTemplate>
</Grid.Resources>
This is where you are going to want to possibly use the Attached Behavior that you already have for the DoubleClick.
Otherwise, here is the complete code that I use which creates the Attached Behavior and will create two Attached Properties which bind to a Command and optionally a Command Parameter.
AttachedBehaviors.cs
public static class MouseDoubleClick
{
public static DependencyProperty CommandProperty =
DependencyProperty.RegisterAttached("Command",
typeof(ICommand),
typeof(MouseDoubleClick),
new UIPropertyMetadata(CommandChanged));
public static DependencyProperty CommandParameterProperty =
DependencyProperty.RegisterAttached("CommandParameter",
typeof(object),
typeof(MouseDoubleClick),
new UIPropertyMetadata(null));
public static void SetCommand(DependencyObject target, ICommand value)
{
target.SetValue(CommandProperty, value);
}
public static void SetCommandParameter(DependencyObject target, object value)
{
target.SetValue(CommandParameterProperty, value);
}
public static object GetCommandParameter(DependencyObject target)
{
return target.GetValue(CommandParameterProperty);
}
private static void CommandChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
Control control = target as Control;
if (control != null)
{
if ((e.NewValue != null) && (e.OldValue == null))
{
control.MouseDoubleClick += OnMouseDoubleClick;
}
else if ((e.NewValue == null) && (e.OldValue != null))
{
control.MouseDoubleClick -= OnMouseDoubleClick;
}
}
}
private static void OnMouseDoubleClick(object sender, RoutedEventArgs e)
{
Control control = sender as Control;
ICommand command = (ICommand)control.GetValue(CommandProperty);
object commandParameter = control.GetValue(CommandParameterProperty);
if (command.CanExecute(commandParameter))
command.Execute(commandParameter);
}
}
.xaml - Remember to add the namespace of where the Attached Behavior lies.
<telerik:RadTreeView IsExpandOnSingleClickEnabled="True"
IsLineEnabled="True"
Margin="5"
ItemsSource="{Binding ItemsView}"
SelectedItem="{Binding SelectedWidget, Mode=TwoWay}"
ItemTemplate="{StaticResource NodeTemplate}"
acb:MouseDoubleClick.Command="{Binding ShowItemCommand}" />
SampleViewModel.cs
private RelayCommand _showItemCommand;
public RelayCommand ShowItemCommand
{
get
{
return _showItemCommand ?? (_showItemCommand =
new RelayCommand(ShowItemDetails, IsItemSelected));
}
}
obviously I don't have Telerik code so I can't really help as much as i would like to but you can try something like this. (Disclaimer: I am writing from top of my head)
Define your style in Grid.Resources
<Style TargetType="{x:Type RadTreeViewItem }" x:Key="TreeViewItemStyle">
<EventSetter Event="MouseDoubleClick" Handler="{Binding DoubleClick}" />
</Style>
Add the Style to Container Style.
<telerik:RadTreeView IsExpandOnSingleClickEnabled="True" IsLineEnabled="True" Margin="5"
ItemsSource="{Binding ItemsView}"
SelectedItem="{Binding SelectedWidget, Mode=TwoWay}"
ItemTemplate="{StaticResource NodeTemplate}"
ItemContainerStyle ="{StaticResource TreeViewItemStyle}"/>
Let me know if it works.
I tried several ways to get this accomplished.In the end I found that VS2012 was giving me fits. I noticed that changes weren't being applied on a build and run.
I opened VS2010 to find I wasn't experiencing the same issues. After speaking with my boss, we found this to be a good example of a situation that adhering to MVVM may not be the wisest choice since double clicking was strictly UI related.
I simply bounced through the code behind and into the view model using the instantiation of the view model as the data context. Didn't take but a second to do that.
As for the other solutions, I am sure it is completely possible, but I cannot confirm or deny the posts I've made here because of my VS2012 issues.
I have the following Attached Property:-
public partial class GridViewProperties
{
public static readonly DependencyProperty DoAutoSizeColumnsProperty =
DependencyProperty.RegisterAttached("DoAutoSizeColumns",
typeof(bool),
typeof(GridViewProperties),
new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault |
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsArrange |
FrameworkPropertyMetadataOptions.AffectsParentArrange |
FrameworkPropertyMetadataOptions.AffectsParentMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
DoAutoSizeColumnsChanged));
public static bool GetDoAutoSizeColumns(DependencyObject obj)
{
return (bool)obj.GetValue(DoAutoSizeColumnsProperty);
}
public static void SetDoAutoSizeColumns(DependencyObject obj, bool value)
{
obj.SetValue(DoAutoSizeColumnsProperty, value);
}
private static void DoAutoSizeColumnsChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var gv = obj as GridView;
if (gv == null)
return;
if (e.NewValue != null && (bool)e.NewValue)
{
AutoSizeColumns(gv.Columns);
SetDoAutoSizeColumns(gv, false);
gv.InvalidateProperty(GridView.ColumnCollectionProperty);
}
}
private static void AutoSizeColumns(GridViewColumnCollection gvcc)
{
// same code as double clicking column gripper
foreach (var gvc in gvcc)
{
// if already set to auto, toggle it so that we can re-run width="auto"
//if (double.IsNaN(gvc.Width))
gvc.Width = gvc.ActualWidth;
// now do it
gvc.Width = double.NaN;
//gvc.InvalidateProperty(GridViewColumn.WidthProperty);
//gvc.ClearValue(GridViewColumn.WidthProperty);
}
}
}
I use it in XAML in the following fashion:
<Style x:Key="AutoColumnStyle" TargetType="ListView">
<Setter Property="View">
<Setter.Value>
<GridView infra:GridViewProperties.AutoSizeColumns="{Binding Path=DataContext.DoAutoSizeColumns, Source={x:Reference uc}}">
<GridViewColumn Width="auto" Header="Title" CellTemplate="{StaticResource Name}" />
<GridViewColumn Width="auto" Header="First" CellTemplate="{StaticResource First}"/>
<GridViewColumn Width="auto" Header="Last" CellTemplate="{StaticResource Last}"/>
</GridView>
</Setter.Value>
</Setter>
The abaove is in UserControl.Resources.
The rest of the XAML is:
<ListView ItemsSource="{Binding Names}" VerticalAlignment="Top" HorizontalAlignment="Left"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
SelectionMode="Single"
x:Name="ListViewContracts"
KeyboardNavigation.TabNavigation="Continue"
KeyboardNavigation.DirectionalNavigation="Cycle"
Style="{StaticResource AutoColumnStyle}"
>
<ListView.ItemContainerStyle >
<Style BasedOn="{StaticResource ListViewItemContainerStyle}" TargetType="ListViewItem">
<Setter Property="Focusable" Value="False" />
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</ListView.ItemContainerStyle>
</ListView>
(I have tried this with no Width="auto" too).
Whenever I set DoAutoSizeColumns = true in my ViewModel I see everything work as expected in the attached property except what it is desgined for that is the gridview columns are not resized according to the largest item in that column (even though I see gv.Width toggled from and to double.Nan which is how resize is meant to work).
As you can see I have tried a number of variations in the attached property mostly commented out including adding in all the FrameworkPoprertyMetadataOptions and trying various InvalidatePoperty attempts but also UIPropertyMetadata too (and DynamicResource too).
What am I missing?
UPDATE
This attached property works in other GridViews the only difference I can see here is that - I need to switch GridViews in the ListView which was not indicated above -but the key difference is that I inject as a style rather than directly. (On second thoughts this may not be the case since the firs item in a column in these other GridViews is always the largest item).
It was to do with the injection of a style.
I had two different GridViews switched by a boolean and used a DataTrigger to inject the alternate one and AutoSizeColumns worked fine on that altererate GridView! So I made both styles be injected by a DataTrigger rather having the default set by just a setter as in the XAML above.
As to why this is case I would be interested in finding out. At least I have a fully MVVM way of autosizing columns which I have not seen in other SO answers.
(BTW I have other attached properties that do not need me to manually trigger the autosize, I just wrote this version to identify the problem).
I'm a little surprised that it is not possible to set up a binding for Canvas.Children through XAML. I've had to resort to a code-behind approach that looks something like this:
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
DesignerViewModel dvm = this.DataContext as DesignerViewModel;
dvm.Document.Items.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(Items_CollectionChanged);
foreach (UIElement element in dvm.Document.Items)
designerCanvas.Children.Add(element);
}
private void Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
ObservableCollection<UIElement> collection = sender as ObservableCollection<UIElement>;
foreach (UIElement element in collection)
if (!designerCanvas.Children.Contains(element))
designerCanvas.Children.Add(element);
List<UIElement> removeList = new List<UIElement>();
foreach (UIElement element in designerCanvas.Children)
if (!collection.Contains(element))
removeList.Add(element);
foreach (UIElement element in removeList)
designerCanvas.Children.Remove(element);
}
I'd much rather just set up a binding in XAML like this:
<Canvas x:Name="designerCanvas"
Children="{Binding Document.Items}"
Width="{Binding Document.Width}"
Height="{Binding Document.Height}">
</Canvas>
Is there a way to accomplish this without resorting to a code-behind approach? I've done some googling on the subject, but haven't come up with much for this specific problem.
I don't like my current approach because it mucks up my nice Model-View-ViewModel by making the View aware of it's ViewModel.
<ItemsControl ItemsSource="{Binding Path=Circles}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="White" Width="500" Height="500" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Fill="{Binding Path=Color, Converter={StaticResource colorBrushConverter}}" Width="25" Height="25" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Top" Value="{Binding Path=Y}" />
<Setter Property="Canvas.Left" Value="{Binding Path=X}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
Others have given extensible replies on how to do what you actually want to do already. I'll just explain why you couldn't bind Children directly.
The problem is very simple - data binding target cannot be a read-only property, and Panel.Children is read-only. There is no special handling for collections there. In contrast, ItemsControl.ItemsSource is a read/write property, even though it is of collection type - a rare occurence for a .NET class, but required so as to support the binding scenario.
ItemsControl is designed for creating dynamic collections of UI controls from other collections, even non-UI data collections.
You can template an ItemsControl to draw on a Canvas. The ideal way would involve setting the backing panel to a Canvas and then setting the Canvas.Left and Canvas.Top properties on the immediate children. I could not get this to work because ItemsControl wraps its children with containers and it is hard to set the Canvas properties on these containers.
Instead, I use a Grid as a bin for all of the items and draw them each on their own Canvas. There is some overhead with this approach.
<ItemsControl x:Name="Collection" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:MyPoint}">
<Canvas HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Ellipse Width="10" Height="10" Fill="Black" Canvas.Left="{Binding X}" Canvas.Top="{Binding Y}"/>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Here's the code behind that I used to set up the source collection:
List<MyPoint> points = new List<MyPoint>();
points.Add(new MyPoint(2, 100));
points.Add(new MyPoint(50, 20));
points.Add(new MyPoint(200, 200));
points.Add(new MyPoint(300, 370));
Collection.ItemsSource = points;
MyPoint is a custom class that behaves just like the System version. I created it to demonstrate that you can use your own custom classes.
One final detail: You can bind the ItemsSource property to any collection you want. For example:
<ItemsControls ItemsSource="{Binding Document.Items}"><!--etc, etc...-->
For further details about ItemsControl and how it works, check out these documents: MSDN Library Reference; Data Templating; Dr WPF's series on ItemsControl.
internal static class CanvasAssistant
{
#region Dependency Properties
public static readonly DependencyProperty BoundChildrenProperty =
DependencyProperty.RegisterAttached("BoundChildren", typeof (object), typeof (CanvasAssistant),
new FrameworkPropertyMetadata(null, onBoundChildrenChanged));
#endregion
public static void SetBoundChildren(DependencyObject dependencyObject, string value)
{
dependencyObject.SetValue(BoundChildrenProperty, value);
}
private static void onBoundChildrenChanged(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs e)
{
if (dependencyObject == null)
{
return;
}
var canvas = dependencyObject as Canvas;
if (canvas == null) return;
var objects = (ObservableCollection<UIElement>) e.NewValue;
if (objects == null)
{
canvas.Children.Clear();
return;
}
//TODO: Create Method for that.
objects.CollectionChanged += (sender, args) =>
{
if (args.Action == NotifyCollectionChangedAction.Add)
foreach (object item in args.NewItems)
{
canvas.Children.Add((UIElement) item);
}
if (args.Action == NotifyCollectionChangedAction.Remove)
foreach (object item in args.OldItems)
{
canvas.Children.Remove((UIElement) item);
}
};
foreach (UIElement item in objects)
{
canvas.Children.Add(item);
}
}
}
And using:
<Canvas x:Name="PART_SomeCanvas"
Controls:CanvasAssistant.BoundChildren="{TemplateBinding SomeItems}"/>
I don't believe its possible to use binding with the Children property. I actually tried to do that today and it errored on me like it did you.
The Canvas is a very rudimentary container. It really isn't designed for this kind of work. You should look into one of the many ItemsControls. You can bind your ViewModel's ObservableCollection of data models to their ItemsSource property and use DataTemplates to handle how each of the items is rendered in the control.
If you can't find an ItemsControl that renders your items in a satisfactory way, you might have to create a custom control that does what you need.