Im trying to make the contents of a List thats a dependency property show up in a WPF context menu.
I have a class with the following dependency property, a list of Foo's (data holding class):
public List<Foo> FooList
{
get { return (List<Foo>)GetValue(FooListProperty); }
set { SetValue(FooListProperty, value); }
}
public static DependencyProperty FooListProperty =
DependencyProperty.Register("FooList", typeof(List<Foo>),
typeof(FooButton));
In XAML I set up the following static resource, I assume its needed since the context menu isnt part of the visual tree:
<UserControl.Resources>
<ResourceDictionary>
<CollectionViewSource
x:Key="FooListSource"
Source="{Binding FooList}"/>
<!-- ... -->
</ResourceDictionary>
</UserControl.Resources>
Also part of the ResourceDictionary above is a CompositeCollection which is needed to make the items show up in the actual context menu. If the UserControl CanStop property is true, we also show a separator and a stop command. These bindings does also fail, although the MenuItems themselves show up. So If I can figure out why these fail, the List might be easier.
<CompositeCollection x:Key="FooListItems">
<CollectionContainer
Collection="{Binding Source={StaticResource FooListSource}}"/>
<Separator
Visibility="{Binding CanStop,
Converter={StaticResource VisibleIfTrue}}" />
<MenuItem
Command="{x:Static Buttons:FooButton.Stop}"
Header="Stop"
Visibility="{Binding CanStop,
Converter={StaticResource VisibleIfTrue}}"/>
</CompositeCollection>
And finally the context menu itself, also in the ResourceDictionary:
<ContextMenu
x:Key="FooButtonMenu"
ItemsSource="{Binding Source={StaticResource FooListItems}}"
ItemTemplate="{StaticResource FooListTemplate}"
<ContextMenu.CommandBindings>
<CommandBinding
Command="{x:Static Buttons:FooButton.Stop}"
Executed="Stop_Executed" />
</ContextMenu.CommandBindings>
</ContextMenu>
I feel Im posting way to much code but Im not sure I can make this piece any simpler. Only the separator and the hardcoded menuitem shows up. So something must be messed up with the bindings. Bindings are usually not that hard but now when I want to bind something thats not really part of the same tree I feel a bit lost.
Any suggestions are welcome. :)
As you suspected, your problem does seem to be caused by the use of List<Foo> instead of ObservableCollection<Foo>. Since List<Foo> doesn't notify on property changes, the only way to get WPF to recognize you've added or removed an item is to temporarily set the FooList property to something else and then set it back.
There is no need to switch to a CLR property. Just change List<Foo> to ObservableCollection<Foo>.
The reason the bindings in your CompositeCollection aren't working is that CompositeCollection is not a DependencyObject, so it can't inherit a DataContext.
I don't see why you've made FooList a dependency property. You're not making it the target of a binding, which is the most common reason to create a dependency property. You haven't implemented a callback, so it can't do change notification (the second most common reason to create a dependency property). You're not using it for value inheritance. So why, then?
It seems to me that what you really want is for FooList to be a normal CLR property of type ObservableCollection<Foo> (or any class that implements INotifyCollectionChanged). That will do all the change notification that you need - at least, that you need for the code you've posted so far.
Related
I have a web app built with ASP.NET and React. I’m trying to port some components to a Windows WPF app, and this is my first time using WPF/XAML. My experience with XAML hasn’t been too bad... it’s like a more-verbose React, but one thing I can’t work out how to do properly is save bind parameters from nested lists to ItemsControl.
In React, I’d use something like onClick=“(e)=>this.myFunction(e, parentIndex)”, so that I could send 2 parameters at once, maybe representing an index and a value, or a childIndex and parentIndex, etc.
It is unclear to me how to properly do this with WPF’s flavour of XAML, and XAML in general. From what I’ve read, I have to use a ICommand (which I feel like is overkill, as the params I’m using are usually not user-input, and instead are references to other objects on the back-end) or set the tag of the initiating object (a button) to a custom class with the amount of attributes I need.
The second approach seems more sensible to me, but I can’t work out how to dynamically do this with an ItemsControl on the XAML frontend — all the tutorials I’ve seen do this in the codebehind, which I don’t think is possible as I’m using an ItemsControl.
How can I do this?
You can pass item to command via CommandParameter.
For assign command to mouseEvents you can use InputBindings.
Lets assume that you have ViewModel with command
public ICommand SomeClickCommand {get; private set}
public void SomeCLickCOmmandHandler (object parameter)
{
var yourItem = object as SomeItemType;
}
you can assign item to command in DataTemplate
<DataTemplate TargetType={x:Type someItemType}>
<Border>
<Border.InputBindings>
<MouseBinding Command="{Binding Path=DataContext.SomeClickCommand , RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}, Mode=FindAncestor}}"
CommandParameter="{Binding}"
Gesture="LeftDoubleClick" />
</Border.InputBindings>
<TextBlock Text={Binding SomeProperty} />
</Border>
</DataTemplate>
I have ListView control in my view with it's own viewmodel A. I have made a seperate UserControl to use as ListViewItem, because it's styling takes a lot of space. In this ListViewItem I have a button, which is binded to viewmodel A and it works fine.
As the context menu has it's own visual tree and cannot bind via ancestor, I have used binding proxy, to solve this issue. I have tweaked it a little so it worked for my particular case, because if it just used {Binding} it would bind to item's model, not listview's viewmodel.
<helpers:BindingProxy x:Key="proxy" Data="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ListView}}"/>
To check if the binding is correct I've used a converter as a way just to have a breakpoint to check source. Everything was good and I was getting my viewmodel right there.
Now, when I try to bind to this in my context menu
<UserControl.ContextMenu>
<ContextMenu>
<MenuItem Header="Open"
Command="{Binding DataContext.OpenChatCommand, Source={StaticResource proxy}, Converter={StaticResource DataBindingDebugConverter}}"
CommandParameter="{Binding}"/>
</ContextMenu>
</UserControl.ContextMenu>
The command never gets called. I added converter to see if something is wrong, but it turns out, I never get to my converter, which in turn means this code never gets executed.
Anyone with any ideas why this is happening and how to solve this is welcome.
I think it's the compiler malfunctioning though
I just did a brief readup on that "binding proxy" you mentioned, but as far as I know, DataGridTextColumn is in the same Visual Tree as its DataGrid, just that its DataContext is bound to its data.
For ContextMenu, it's totally different. This one really has a separate tree from its parent. There is no point in using a proxy object in resources, because it is from a different visual tree. When you use StaticResource, WPF will search upwards through its visual tree, level by level, inside those elements' Resource property (which is a ResourceDictionary).
One way is to make that proxy into a singleton, and use Source={x:Static helpers:BindingProxy.Instance}. Of course using this means that your proxy can only be used by a single View, or else something unexpected would happen.
The other way is to make use of PlacementTarget property of the ContextMenu.
<ContextMenu DataContext="{Binding Path=PlacementTarget.DataContext,
RelativeSource={RelativeSource Self}}">
This is the preferred way, but you need to make sure the parent's DataContext is really the VM that you need.
Edit
There is no super elegant way to do it the MVVM way. The best way is probably through the use of Tag property.
<ContextMenu DataContext="{Binding Path=PlacementTarget.Tag,
RelativeSource={RelativeSource Self}}">
ListView Control:
<MyControl:MyListViewItem .... Tag="{Binding Path=DataContext, RelativeSource={RelativeSource AncestorType={x:Type MyControl:MyListViewView}}}"}" ...>
I tried to follow the example here:
WPF ListBox with self-removing items
It made sense but my issue was, the ListView itself is determining the template used. So it can easily customise the bindings to point to the correct target. I am however using MVVM and am struggling to fit the two together.
Example, if the template was:
<ListBox.ItemTemplate>
<DataTemplate>
<local:MyItemView/>
</DataTemplate>
</ListBox.ItemTemplate>
This suddenly becomes more difficult, as ideally, I want to reuse that view without hard coding the bindings.
I tried to use DependencyProperty to pass the List and the Element through, so I could delete it via command.
<ListBox.ItemTemplate Name="myList">
<DataTemplate>
<local:MyItemView TheList={Binding ElementName=myList, Path=DataContext.List} TheElement={Binding}/>
</DataTemplate>
</ListBox.ItemTemplate>
However, I had binding errors telling me that it couldn't convert the value for TheElement from MyClassViewModel to MyClass. Even if I commented that out TheList was always NULL.
Essentially I want:
class MyDataClass { // pretend there's more here}
class MyDataClassContainer
{
public ObservableCollection<MyDataClass> Items;
public void Add(MyDataClass);
public void Remove(MyDataClass);
}
class MyDataClassEntryViewModel
{
public static readonly DependencyProperty ListItemProperty = DependencyProperty.Register("TheClass", typeof(MyDataClass), typeof(MyDataClassEntryViewModel));
public static readonly DependencyProperty ListContainerProperty = DependencyProperty.Register("TheContainer", typeof(MyDataClassContainer), typeof(MyDataClassEntryViewModel));
public MyDataClass TheClass;
public MyDataClassContainer TheContainer;
public ICommand Delete = new DelegateCommand(RemoveItem);
private function RemoveItem(object parameter)
{
TheContainer.Remove(TheClass);
}
}
With the following templates:
MyDataClassEntryView.xaml
<UserControl>
<Grid>
<Button Content="Delete" Command="{Binding Path=Delete}"/>
</Grid>
</UserControl>
MyDataContainerView.xaml
<UserControl>
<ListView x:Name="listView" ItemsSource="{Binding Path=Container.Items}">
<ListView.ItemTemplate>
<DataTemplate>
<local:MyDataClassEntryView TheClass="{Binding}" TheContainer="{Binding ElementName=listView, Path=DataContext.Container}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</UserControl>
Note: I have omitted most of the superfluous lines, as I'm trying to get a generic answer I can use everywhere. Not a hard coded single solution. I was basically want to keep the MVVM structure strong, without lots of hard coded and wiring in the background. I want to use the XAML as much as possible.
All the other methods I see to do with removing from a list, require all sorts of assumptions, such as using the SelectedIndex/Item, or using a method on the ContainerView itself to take the element as a parameter, cast it, then remove, etc. In short, most solutions are far too hard coded to the given examples. It feels like there should be an easy way to achieve this in WPF.
As the ListView issautomatically creating instances of my sub-ViewModel/Views, it's impossible for me to get any data in apparently. I just want to pass parameters along using bindings, basically.
Your button should look like this:
<Button Content="Delete"
Command="{Binding Path=Delete}"
CommandParameter="{Binding}/>
Then the remove command should look something like this:
private function RemoveItem(object parameter)
{
var item = parameter as MyDataClass
if(item != null)
TheContainer.Remove(item);
}
You do not need to pass the list to the UserControl within the ItemTemplate, since it doesn't need to know about the list at all
Edit:
I read over your question a few times to see what you were confused about so I will try to clarify.
Whether the ListView sets its own template in the Xaml, or you use another UserControl, the datacontext still gets passed down to the item. Regardless of how you decide to template the items, the ItemTemplate will have the datacontext of a single item from the ListView's items list.
I think your confusion comes in with having controls outside being brought in for templating. Think of it as if the Xaml from the control you brought in being cut and pasted into the DataTemplate of the ListView when running the program, and then it is really no different from being hard coded in there.
You cannot reach outside of a DataTemplate with Element bindings like you have tried.
Instead you need to use a relativesource like this.
<local:MyItemView TheList="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ListBox}, Path=DataContext.List}" />
I have a listbox in WPF that will contain a list of ResultsViewModel items, however the actual runtime type of these objects could be
CalculateResultsViewModel,
ScenarioResultsViewModel,
GraphResultsviewModel etc etc,
all of which extend the base abstract class ResultsViewModel.
Each of these view models should be rendered differently in the ListBox so needs a different DataTemplate. I can do that just with XAML easy enough. The difficulty is that when the viewmodels are either "processing" or when they have failed", I need them to display a DataTemplate for "processing" or "errored" which I can only so far do with Triggers. That however then means I can't use the DataTemplateSelector or a basic XAML style.
The only solution I can think of (not clean I know) is to set the DataTemplate programmatically in the SetResult() method of each viewmodel class, which is what gets called when the processing completes either successfully or with an error. In that DependencyProperty I can look at the return code and then programatically set the DataTemplate depending on the sucess/failure result. The only problem is I cannot figure out how to
Obtain a DataTemplate resource from a ResourceDictionary just using c# code - bearing in mind Im calling all of this from the viewmodel class, not the window code-behind .xaml.cs file so it doesn't have access to the properties of Window
having only a handle to the viewmodel class, somehow obtain a reference to the ListBoxItem that contains it and then programmatically set the DataTemplate on this container.
Can anyone point me in the right direction?
you can take the magic with implicit datatemplates
<ListBox ItemSource={Binding YourResults}>
<ListBox.Resources>
<DataTemplate DataType={x:Type CalculateResultsViewModel}>
<Grid></Grid>
</DataTemplate>
<DataTemplate DataType={x:Type ScenarioResultsViewModel}>
<Grid></Grid>
</DataTemplate>
<DataTemplate DataType={x:Type GraphResultsviewModel }>
<Grid></Grid>
</DataTemplate>
</ListBox.Resources>
</ListBox>
for "processing" or "errored" viewmodels you can specify a adorner overlay in all yout datatemplates (ok but you must use the triggers)
hope this helps
I'm trying to choose the best way to implement this UI in MVVM manner. I'm new to WPF (like 2 month's) but I have huge WinForms experience.
The ListBox here act's like a TabControl (so it switches the view to the right), and contains basically the Type of item's displayed in tables. All UI is dynamic (ListBox items, TabItems and Columns are determined during run-time). The application is targeting WPF and Silverlight.
Classes we need for ViewModel:
public abstract class ViewModel : INotifyPropertyChanged {}
public abstract class ContainerViewModel : ViewModel
{
public IList<ViewModel> Workspaces {get;set;}
public ViewModel ActiveWorkspace {get;set;}
}
public class ListViewModel<TItem> where TItem : class
{
public IList<TItem> ItemList { get; set; }
public TItem ActiveItem { get; set; }
public IList<TItem> SelectedItems { get; set; }
}
public class TableViewModel<TItem> : ListViewModel<TItem> where TItem : class
{
public Ilist<ColumnDescription> ColumnList { get; set; }
}
Now the question is how to wire this to View.
There are 2 base approaches I can see here:
With XAML: due to lack of Generics support in XAML, I will lose strong typing.
Without XAML: I can reuse same ListView<T> : UserControl.
Next, how to wire data, I see 3 methods here (with XAML or without doesn't matter here). As there is no simple DataBinding to DataGrid's Columns or TabControl's TabItems the methods I see, are:
Use DataBinding with IValueConverter: I think this will not work with WPF|Silverlight out of the box control's, as some properties I need are read-only or unbindable in duplex way. (I'm not sure about this, but I feel like it will not work).
Use manual logic by subscribing to INotifyPropertyChanged in View: ViewModel.PropertyChanged+= ....ViewModel.ColumnList.CollectionChanged+= ....
Use custom controll's that support this binding: Code by myself or find 3d party controls that support this binding's (I don't like this option, my WPF skill is too low to code this myself, and I doubt I will find free controls)
Update: 28.02.2011
Things get worser and worser, I decided to use TreeView instead of ListBox, and it was a nightmare. As you probably guess TreeView.SelectedItems is a readonly property so no data binding for it. Ummm all right, let's do it the old way and subscribe to event's to sync view with viewmodel. At this point a suddenly discovered that DisplayMemberPath does nothing for TreeView (ummmm all right let's make it old way ToString()). Then in View's method I try to sync ViewModel.SelectedItem with TreeView's:
private void UpdateTreeViewSelectedItem()
{
//uiCategorySelector.SelectedItem = ReadOnly....
//((TreeViewItem) uiCategorySelector.Items[uiCategorySelector.Items.IndexOf(Model.ActiveCategory)]).IsSelected = true;
// Will not work Items's are not TreeViewItem but Category object......
//((TreeViewItem) uiCategorySelector.ItemContainerGenerator.ContainerFromItem(Model.ActiveCategory)).IsSelected = true;
//Doesn't work too.... NULL // Changind DataContext=Model and Model = new MainViewModel line order doesn't matter.
//Allright.. figure this out later...
}
And none of methods I was able to think of worked....
And here is the link to my sample project demonstrating Control Library Hell with MVVM: http://cid-b73623db14413608.office.live.com/self.aspx/.Public/MVVMDemo.zip
Maciek's answer is actually even more complicated than it needs to be. You don't need template selectors at all. To create a heterogeneous tab control:
Create a view model class for each type of view that you want to appear as tab items. Make sure each class implements a Text property that contains the text that you want to appear in the tab for its item.
Create a DataTemplate for each view model class, with DataType set to the class's type, and put the template in the resource dictionary.
Populate a collection with instances of your view models.
Create a TabControl and bind its ItemsSource to this collection, and add an ItemTemplate that displays the Text property for each item.
Here's an example that doesn't use view models (and that doesn't implement a Text property either, because the objects I'm binding to are simple CLR types), but shows how template selection works in this context:
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:coll="clr-namespace:System.Collections;assembly=mscorlib">
<DockPanel>
<DockPanel.Resources>
<coll:ArrayList x:Key="Data">
<sys:String>This is a string.</sys:String>
<sys:Int32>12345</sys:Int32>
<sys:Decimal>23456.78</sys:Decimal>
</coll:ArrayList>
<DataTemplate DataType="{x:Type sys:String}">
<TextBlock Text="{Binding}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type sys:Int32}">
<StackPanel Orientation="Horizontal">
<TextBlock>This is an Int32:</TextBlock>
<TextBlock Text="{Binding}"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type sys:Decimal}">
<StackPanel Orientation="Horizontal">
<TextBlock>This is a Decimal: </TextBlock>
<TextBlock Text="{Binding}"/>
</StackPanel>
</DataTemplate>
</DockPanel.Resources>
<TabControl ItemsSource="{StaticResource Data}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
</DockPanel>
</Page>
Of course in a real MVVM application those DataTemplates would use UserControls to map each type to its view:
<DataTemplate DataType="{x:Type my:ViewModel}">
<my:View DataContext="{Binding}"/>
</DataTemplate>
Maciek and Robert already gave you some ideas on how to implement this.
For the specifics of binding the columns of the DataGrid I strongly recommend Meleak's answer to that question.
Similar to that you can use attached properties (or Behaviors) and still maintain a clean ViewModel in MVVM.
I know the learning curve for WPF is quite steep and you're struggling already. I also know that the following suggestion doesn't help that and even makes that curve steeper. But your scenario is complex enough that I'd recommend to use PRISM.
I wrote an article and a sample application with source code available, where I discuss and show the problems I have mentioned here and how to solve them.
http://alexburtsev.wordpress.com/2011/03/05/mvvm-pattern-in-silverlight-and-wpf/
In order to connect your ViewModel to your View you need to assign the View's DataContext. This is normally done in the View's Constructor.
public View()
{
DataContext = new ViewModel();
}
If you'd like to see your view model's effect at design time, you need to declare it in XAML, in the View's resources, assign a key to it, and then set the target's DataContext via a StaticResource.
<UserControl
xmlns:vm="clr-namespace:MyViewModels
>
<UserControl.Resources>
<vm:MyViewModel x:Key="MyVM"/>
</UserControl.Resources>
<MyControl DataContext={StaticResource MyVM}/>
</UserControl>
(The above is to demonstrate the design-time trick works)
Since you're dealing with a scenario that includes a container such as the TabControl I'd advocate considering the following things :
Hold your child ViewModels in a Property of type ObservableCollection
Bind the TabControls ItemsSource to that property
Create a new View that derives from TabItem
Use a template selector to automatically pick the type of the view based on the type of the view model.
Add IDisposable to yoour child ViewModels and create functionality to close the views.
Hope that helps a bit, if you have further questions let me know.