Getting at the Listbox's ItemContainer when data binding - silverlight

Is there a way to get at the ItemContaner of a selected item in a listbox? In Silverlight 2.0 Beta 1 I could, but the container is hidden in Beta 2 of Silverlight 2.0.
I'm trying to resize the listbox item when it is unselected to a specific size and when selected to a variable size. I also want to get the relative position of the selected item for animations. Growing to a variable size and getting the relative pasition is why i need to get to the listbox item.
I should clarify i'm not adding items to the listbox explicitly. I am using data binding in xaml and DataTemplates. What I have trouble accessing is the ItemContainer of the selected item's DataTemplate.

There is a way to obtain the Panel containing the item's UIElement and the mapping of items to UIElements. You have to inherit from ListBox (this actually works for any ItemsControl) and override PrepareContainerForItemOverride:
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
base.PrepareContainerForItemOverride(element, item);
var el = element as FrameworkElement;
if (el != null)
{
// here is the elements's panel:
_itemsHost = el.Parent as Panel;
// item is original item inserted in Items or ItemsSource
// we can save the mapping between items and FrameworElements:
_elementMapping[item] = el;
}
}
This is kind of hackish, but it works just fine.

If you are adding non-UI elements to the listbox (such as strings or non-UI data objects), then this is probably pretty difficult. However if you wrap your items in some sort of FrameworkElement-derived object before adding them to the listbox, you can use TransformToVisual to get the relative size and use Height and Width to set the size of the item.
In general you can wrap your objects in a ContentControl like the following. Instead of:
_ListBox.Items.Add(obj0);
_ListBox.Items.Add(obj1);
Do this:
_ListBox.Items.Add(new ContentControl { Content = obj0 });
_ListBox.Items.Add(new ContentControl { Content = obj1 });
Now when you get _ListBox.SelectedItem you can cast it to ContentControl and set the size and get the relative position. If you need the original object, simply get the value of the item's Content property.

It appears that you can use relative binding to get at the Item Container from the ItemTemplate.
<TextBlock YourTargetProperty="{Binding RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type ListBoxItem}}, Mode=OneWay, Path=YourSourceProperty}" />
I found this solution here.

Update for silverlight 5.
<ListBox ItemsSource="{Binding Properties}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListBoxItem}}" />
</DataTemplate>
</ListBox.ItemTemplate>
RelativeSource AncestorType is now supported, making this much easier.

Related

Binding inside ContentControl not working

I'm building a graphical designer, based upon an article by Sukram in CodeProject. I'm now trying to extend it so that each item on the canvas binds to a different ViewModel object - i.e. I'm setting the DataContext for each item.
Every item on the designer is actually a ContentControl, into which is placed a different template (based upon which toolbox item was dragged onto the canvas). So I have a template containing a TextBox, and I have a ViewModel object containing a Name property, and I bind the Text property of the TextBox to the Name property of the ViewModel, and ... nothing. I've checked the visual tree with Snoop, and it confirms that the DataContext of the TextBox is the ViewModel object. Yet the TextBox remains empty. And if I modify the (empty) Text in the TextBox, the Name property in the ViewModel does not change. So it looks like the binding is not being applied (or has been removed somehow).
I've found a few posts which talk about the ContentControl messing around with the DataContext and Content properties, but I'm not sure how applicable they all are. The code sets the ContentControl.Content as follows:
newItem = new ContentControl();
ControlTemplate template = toolbox.GetTemplate();
UIElement element = template.LoadContent() as UIElement;
ViewModelItem viewModel = new ViewModelItem() { Name = "Bob" };
newItem.Content = element;
newItem.DataContext = viewModel;
and the XAML for the template is:
<ControlTemplate>
<Border BorderBrush="Black" BorderThickness="1" Width="100">
<TextBox Text={Binding Name}/>
</Border>
</ControlTemplate>
Snoop shows that the TextBox has a DataContext, and if I Delve that DataContext I can see that it has a Name property whose value is "Bob". So why does the TextBox remain empty? Snoop allows me to change that Name property, but the TextBox remains empty.
What am I doing wrong?
A few more details. I've set the VS2010 Debug DataBinding option for the OutputWindow to Verbose, which seems to show that the binding is all being attempted before I set the DataContext. Is it possible that the change to the DataContext is not being recognised?
I've just found this post DataTemplate.LoadContent does not preserve bindings - apparently DataTemplate.LoadContent does not preserve bindings. So it looks like I have to write my own version of LoadContent().
I've realised that the template has come through a XamlWriter, which apparently strips all bindings. This wouldn't be helping.
I've not been able to fix the DataTemplate.LoadContent(), but I realised that I didn't actually need a DataTemplate, since the XamlWriter / XamlReader was already instantiating the UI element that I was after. I found a fix to make the XamlWriter write all the bindings here, and after that it all works.
Thanks for your help.
Maybe you need to tell the binding in the ControlTemplate to look at the TemplatedParent, as is mentioned in this thread?
<TextBox Text="{Binding Path=Name, RelativeSource={RelativeSource TemplatedParent}}"/>
Either that, or try to use a DataTemplate instead.
I can't test this at the moment, so I might just be guessing here.
I would use a DataTemplate, as bde suggests.
You are trying to put some UI on your own data (ViewModel), and this is what Data-Templates are meant for (ControlTemplate is usually what you use if you want to change how e.g. a Button looks).
Change your code to use ContentControl.ContentTemplate with a DataTemplate:
<DataTemplate>
<Border BorderBrush="Black" BorderThickness="1" Width="100">
<TextBox Text={Binding Name}/>
</Border>
</DataTemplate>
Code-behind:
newItem = new ContentControl();
//NOTE: .GetTemplate() needs to return a DataTemplate, and not a ControlTemplate:
newItem.ContentTemplate = toolbox.GetTemplate();
ViewModelItem viewModel = new ViewModelItem() { Name = "Bob" };
newItem.Content = viewModel;
newItem.DataContext = viewModel;

Binding MenuItem's IsChecked to TabItem's IsSelected with dynamic tabs

I have a list of tab items that have views dynamically added to them. Every time a user adds a view, a new tab item is created. I'm now trying to bind a menu to a tabcontrol's items so that a user can select from a menu which view is currently the active view.
My menu is bound as such:
<Menu Background="Transparent">
<MenuItem Style="{StaticResource TabMenuButtonStyle}" ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TabControl}}, Path=Items}" ItemContainerStyle="{StaticResource TabMenuItem}"></MenuItem>
</Menu>
This works fine and has the desired effect (each menu item is a listing of all the open tabs).
I have the following style that binds menu items to the IsSelected property of the tab items:
<Setter Property="IsChecked" Value="{Binding Path=IsSelected, Mode=TwoWay}" />
My problem is, this binding doesn't work. The binding error message is stating that it can't find the IsSelected property on the view object. I don't want it to use the specfic view, rather, I want it to look at the tab item that the view is currently bound to.
I've tried the following, but still get a binding error:
<Setter Property="IsChecked" Value="{Binding Path=IsSelected, Mode=TwoWay, RelativeSource={RelativeSource AncestorType=TabItem}}}" />
Which states that it can't find an ancestor of type TabItem for each menu item (which makes sense as the menu item's ancestors are not what it is bound to.)
Is there any way I can get access to the parent of the item that is coming in as a binding so I can bind to its properties?
Update:
Per Yadyn's advice, I decided to create a value converter and return tab items.
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
ItemCollection ic = (ItemCollection)value;
List<TabItem> tabItems = new List<TabItem>();
foreach (var obj in ic) {
tabItems.Add((TabItem)obj);
}
return tabItems;
}
This makes binding IsSelected to IsChecked work for static items (TabControls that have their tab items already created), but for the dynamically added views, the Convert method never gets called. It's like the TabControl is not sending out an update to binders of its items that something has changed. Here is how the MenuItem is wired up now:
<MenuItem Style="{StaticResource TabMenuButtonStyle}" ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TabControl}}, Path=Items, Mode=OneWay, NotifyOnSourceUpdated=True, Converter={StaticResource TabControlItemConverter}}" ItemContainerStyle="{StaticResource TabMenuItem}"></MenuItem>
TabControl.Items will get you back the views, since that is what you've bound to your TabControl to have dynamic tab views.
Unfortunately, there isn't a property you can bind to on the TabControl directly that will get you a collection of the TabItems. These are actually the ItemContainers for each item in the Items bound collection.
What you might do is create a converter or something. You can try using myTabControl.ItemContainerGenerator.ContainerFromItem and pass in the view object to get back the actual TabItem that wraps it. Then your IsSelected binding will work.
You might consider binding directly to the TabControl itself instead of the Items property. Then the converter can easily do the above call to ContainerFromItem. You'll then have to return a List<TabItems> from the converter by enumerating the Items property yourself (calling ContainerFromItem for each).
Anyway, hopefully this gets you on the right track!
Here is something simpler. Define a toplevel viewmodel that holds a collection of viewmodels representing the tabs and menuitems like so
//Not showing here the details of implementing INPC
public class MyCustomCompositeViewModel:INotifyPropertyChanged
{
public ObservableCollection<CompositeViewItem>CompositeItems{get;set;}
public CompositeViewItem SelectedItem{get;set;}
}
On the view, bind the tabitems to the CompositeItems collection and bind the selected tab item to the selectedItem. You can bind the MenuItems similarly
The compositeviewitem should provide properties like the name of the item (for display on the tab and menu), and perhaps the additional data the View needs for rendering. Hope this makes sense.

Resizing a Panels based on the size of user control

I'm trying to find a way to resize a LayoutPanel (DevExpress) based on the size of the user control that it contains. The user control is exposed using a ContentControl. Here's the relevant Code
This is the Layout panel and the coresponding view:
<dxd:LayoutPanel Caption="Search Criteria" CaptionImage="Images/Icons/DetailView.png">
<ContentControl Name="myContentControl" Content="{Binding Path=ProjectsSearchVM}"/>
</dxd:LayoutPanel>
The ProjectSearchVM is a property of the MainWindowViewModel, which is the DataContext for the code above. This property returns an object of type ProjectsSearchViewModel that is replaced by its corresponding View (containing a userControl) using a Resource File:
<DataTemplate DataType="{x:Type vm:ProjectSearchViewModel}">
<vw:ProjectSearchView />
</DataTemplate>
The problem is that my view is higher than the original size of the Layout Pannel. I'd like to bind the panel's MinSize to the size of my view (or the ContentControl containing it).
I tried this, but it doesn't work:
<dxd:LayoutPanel Caption="Search Criteria" CaptionImage="Images/Icons/DetailView.png">
<dxd:LayoutPanel.MinSize>
<Binding ElementName="myContentControl" Path="Size"/>
</dxd:LayoutPanel.MinSize>
<ContentControl Name="myContentControl" Content="{Binding Path=ProjectsSearchVM}" />
</dxd:LayoutPanel>
I'm still very new to WPF, so I'm sure the solution is simple.
Can anyone enlighten me?
The question was answered on the DevExpress site at this link:
http://www.devexpress.com/Support/Center/Question/Details/Q448884
For flyout, it involves overriding the control in the container, for example, if it's in a Border, something like this:
public class AutoSizeContainer : Border {
protected override void OnInitialized(EventArgs e) {
base.OnInitialized(e);
BaseLayoutItem item = DockLayoutManager.GetLayoutItem(this);
Child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
AutoHideGroup.SetAutoHideSize(item, new Size(Child.DesiredSize.Width + 6, Child.DesiredSize.Height + 6));
}
}
Making such an object the root object of the LayoutPanel in an AutoHide group, makes the flyout sizing correct.
I'm not familiar with DevExpress's LayoutPanel, but most standard WPF panels (Grid, StackPanel) "auto size" to contain their children. Are you setting hard Height and Width properties for the LayoutPanel (or does it only contain a Size property and not Height and Width like standard WPF controls)?
If you are forced to use hard Height/Width values, a common way to bind them would be like this:
<dxd:LayoutPanel ... Height="{Binding Height, ElementName=myContentControl}" Width="{Binding Width, ElementName=myContentControl}">
<ContentControl x:Name=myContentControl ... />
</dxd:LayoutPanel>
Normally, if you bind Heights and Widths you bind to ActualHeight or ActualWidth, but those properties do not exist on ContentControl. (Height and Width are only suggested values, so you may find that if the above binding is needed and works for you, you may need to tweak the values slightly with a value converter to account for margins or padding).

Silverlight 4: how to display list of custom controls (not in list order)

There are following object:
'FieldItem' custom control;
'Field' - ... XAML-object, which will contains a dozen of field items;
FieldItemViewModel - data class that hosts data to be displayed with 'FieldItem' custom control;
position of 'FieldItem' control depend from data entity parameters that is bounded to the control (X and Y);
items - ObservableCollection - collection that contains a data.
Question: what kind of object should I put inside of the in order to have each item of the my FieldItems to be displayed inside of Canvas?
I've planned to use ListView... but... can't imagine how is it possible to change position of the list view item...
Any thoughts are welcome!
Thanks.
You can have a simple ItemsControl.
ItemsControl is just a container of items.
The ItemsPanel should be set to your canvas. And the data template of each item should be the 'FieldItem' control.
In your viewmodel expose a property that is called Items which will be a collection of the items data.
Something similar to this:
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<FieldItem />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
Silverlight doesn't have ItemContainerStyle but you can set it in code:
public class MyItemsControl : ItemsControl
{
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
FrameworkElement contentitem = element as FrameworkElement;
Binding leftBinding = new Binding("Position.X");
Binding topBinding = new Binding("Position.Y");
contentitem.SetBinding(Canvas.LeftProperty, leftBinding);
contentitem.SetBinding(Canvas.TopProperty, topBinding);
base.PrepareContainerForItemOverride(element, item);
}
}
Taken from here: http://forums.silverlight.net/forums/p/29753/96429.aspx

Render List<Canvas> as list items (or itemscontrol)

I've effectively got a List or a List, XAML that represents the canvas elements... as a return from some stuff I have no control of.
I've been moderately successful rendering via stackpanel.children.add etc, however want to start having the list of canvas within a virtualizing panel or list.
I've set itemssource and datacontext on the <ItemsControl> and set the <DataTemplate> as such
<DataTemplate>
<ContentControl content="{Binding Path=CanvasBody}"/>
</DataTemplate>
This effectively turns entire silverlight body white/blank. I dont really care how I ultimately get to the desired result which is a list of the rendered canvas's... preferably virtualized for speed.
Its a retarded problem, and not ideal as far as how silverlight apps are built, I know...
I'd really appreciate any pointers. THANKS!
Generally to display a list of elements you bind the items control itemssource property to the list and then set a datatemplate for it which displays the desired properties of the list item type. You dont need to set content control. Beyond that I cant see what exactly you are asking.
<DataTemplate>
<Grid>
<TextBlock Text="{Binding ExampleProperty1}"/>
</Grid>
</DataTemplate>
codebehind file:
public class ExampleClass
{
public String ExampleProperty1
{
get
{
return "TEST";
}
}
}
public List<ExampleClass> List {get;} // note that this must be a public PROPERTY,
// not a field!

Resources