I've got a WPF MultiSelectTreeView (downloaded from here: http://unclassified.software/en/source/multiselecttreeview).
Now I want to control, which items the user selects. A simple example is that he shouldn't be able to select child nodes of different parents. But there are also more ViewModel-specific use cases.
It's easy to achieve this in code-behind of the Window by using the PreviewSelectionChanged event, checking the conditions directly and setting the Cancel-flag accordingly. But since I want to obtain the separation of View and ViewModel, I am looking for a way of doing this in my WindowViewModel.
Of course you could also extract the check to the ViewModel and call it from the view, but it looks wrong:
WindowViewModel _viewModel;
void PreviewSelectionChanged(object sender, PreviewSelectionChangedEventArgs e)
{
e.Cancel = !this._viewModel.CanSelect(e.Item as TreeItemViewModel);
}
I hope that anybody has an idea.
- timnot90
Typically, when data binding a hierarchical collection to a TreeView in WPF, the custom data items should have an IsSelected property defined in their class. If they do, then it can be data bound to the IsSelected property of each TreeViewItem:
<TreeView ItemsSource="{Binding YourCollection}" ... >
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
When this is done, you can just set that property to true to select an item and to false to deselect an item.
// Select Item
dataObject.IsSelected = true;
// Deselect Item
dataObject.IsSelected = false;
You can add a handler to the PropertyChanged event of each item to detect when the IsSelected property changes (if they implement the INotifyPropertyChanged interface as expected).
Related
In my code, I only have one ListView element that goes over a bunch of conditions and MultiDataTriggers to determine the correct template for each ListViewItem that is supposed to be drawn on the display.
This ListView element defines a ListView.ItemContainerStyle which then defines a ContentTemplate property for each item and in this part, I have declared the conditions for selecting each template. [Code Shown Below]
From this design, my understanding is that the responsibility of creating the elements and drawing the appropriate template is on ListView's shoulders, not the ListViewItems. Therefore, when I change something about the DataContext of ListViewItems and call "UpdateLayout()" on them, nothing happens because they don't know how else to re-draw themselves.
The changes that I make to ListViewItems' DataContexts, are made to the same properties that are important when choosing the right template in the beginning. So, the template itself needs to be changed, not the insides of the elements, and this is my problem.
There is an Error class and it has a property named List<string> Suggestions that may have some items inside of it or nothing. When choosing the template, Suggestions.Count is decisive.
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Suggestions.Count}"
Value="0" />
</MultiDataTrigger.Conditions>
<Setter Property="ContentTemplate"
Value="{StaticResource SingleSuggestionErrorTemplate}" />
</MultiDataTrigger>
In run-time, the users see a ListViewItem that has no "More Suggestions" box, but by using a button, they can add a new one for the item. Then, the ListViewItem has to be re-drawn and show the template that displays another set of elements which includes the "More Suggestions" box. Here's an example of code-behind for doing this and also the way I access the parent Items:
async private void SubmitUserSuggestionButton_Click (object sender, RoutedEventArgs e)
{
// Get the Expander element.
Visual exp = ((DependencyObject)e.Source).GetParent<Expander>();
// Access the parent ListViewItem of the Expander
ListViewItem LVItem = exp.GetParent<ListViewItem>();
// Get the location of the Error inside document.
Error LVItem_Error = (Error)LVItem.DataContext;
if (LVItem_Error.Word == "something")
{
LVItem_Error.Suggestions.Add("Custom Suggestion!");
// At this point, the previous template which showed no suggestions
// is no longer valid and the template has to be chosen again,
// based on the new count of the list.
LVItem.UpdateLayout();
return;
}
}
I have tried using ObservableCollection instead of List and failed. Also, I have implemented INotifyPropertyChanged on the collection and that has failed too. The problem with the second approach may be that Collection.Add() does NOT fire PropertyChanged, but I'm not entirely sure that if I handled that problem, it would be useful since the ListViewItem itself is not capable of deciding what template to choose and the upper-level ListView has to do that.
I should mention that I'm aware of ListView.Items.Refresh() and it works for my scenario, but it's not helpful because I don't want and need to re-draw every item inside the list.
At last, What I'm hoping to do is to tell the upper-level ListView to "Refresh" only one specific item of its collection, not the whole bunch. I'd be very grateful for any and all answers or pointers.
The ListView:
<ListView Name="lvErrors">
<!-- Main ListView Item Style -->
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<ContentControl Content="{Binding}">
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<!-- Definition of MultiDataTriggers and Conditions -->
.
.
.
Thanks in advance.
I create a project using MVVM pattern.
In my View I create comboBoxes.
In ViewModel I create ObservableCollection for ItemsSource for comboBoxes with string values:
public ObservableCollection<string> ComboBoxItems
{
get; set;
}
In ViewModel constructor I create list of Models (foreach comboBox).
My Model class have only two properties: SelectedComboBoxItem and IsEnabledComboBoxItem.
I want to have a logic like if I select one item in one comboBox it shoud be disable in this comboBox and in all others. How could I do this with Binding?
Now my xaml code look like this, but it disabled only selected comboBox item and only in one comboBox, where he was called from:
<ComboBox ItemsSource="{Binding ComboBoxItems}"
SelectedItem="{Binding SelectedComboBoxItem }" IsEditable="True">
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Setter Property="IsEnabled" Value="{Binding IsEnabledComboBoxItem}" />
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
To enable/disable comboBox items i used converter for them in xaml,
where passed all selected values from all comboBoxes (collection in ViewModel). In my converter class I did the check I needed and return a bool value(is item in collection in ViewModel or not). Maybe this helps somebody.
I have used the following approach to bind IsSelected of my items to a Property: WPF ListView Programmatically Select Item
<ListView.ItemContainerStyle>
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</ListView.ItemContainerStyle>
Now I am able to select my items in code behind by simple setting the IsSelected property to true. However I am not able to deselect items by setting the IsSelected property of my items to false.
Setting the items property IsSelected to true will trigger the ListViewSelectionChanged event. However setting the property IsSelected of an already selected item to false does not trigger the event. The property will be changed to false but the item remains selected within the ListView. I have also tried using Mode=TwoWay without any success.
I would appreciate any sort of help!
Thank you very much in advance,
Thomas
For OP or others looking to "programmatically" deselect a ListView.
If your ListView rigged up as Single, Extended or Multiple you can always just:
YourlistView.Selecteditem = null;
Or you can use this as well:
YourlistView.UnselectAll();
Looks like you are just missing the TargetType for the style. Add the target type of ListViewItem as per Kent's original code below.
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="IsSelected" Value="{Binding IsGroovy}"/>
</Style>
</ListView.ItemContainerStyle>
I have in my ParentView(DashboardConsultants) a gridview which shows a custom tooltip when the user's mousepointer is hovered over a cell. The tooltip show a View (AgreementDetails_View) which shows information of the Agreement binded to that cell. I will show the code I have now so you can better understand my question:
DataGrid Cell in ParentView:
<DataGridTextColumn Header="Okt" Width="*" x:Name="test">
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Tag" Value="{Binding Months[9].AgreementID}"/>
<Setter Property="Background" Value="White" />
<Setter Property="DataGridCell.ToolTip" >
<Setter.Value>
<v:UC1001_AgreementDetails_View Background="#FFF" Opacity="0.88" />
</Setter.Value>
</Setter>
My ChildView:
public UC1001_DashBoardConsultants_View(UC1001_DashboardConsultantViewModel viewModel)
{
InitializeComponent();
this.DataContext = viewModel;
}
In the ViewModel, I have following method to get the right Agreement from the database:
private void GetRefData()
{
UC1001_ActiveAgreementArguments args = new UC1001_ActiveAgreementArguments();
args.AgreementID = 3;
DefaultCacheProvider defaultCacheProvider = new DefaultCacheProvider();
if (!defaultCacheProvider.IsSet("AgrDet:" + args.AgreementID))
{
ConsultantServiceClient client = new ConsultantServiceClient();
AgreementDetailsContract = client.GetAgreementDetailsByAgreementID(args);
defaultCacheProvider.Set("AgrDet:" + args.AgreementID, AgreementDetailsContract, 5);
}
else
{
AgreementDetailsContract = (UC1001_ActiveAgreementContract)defaultCacheProvider.Get("AgrDet:" + args.AgreementID);
}
}
As you can see for now, the method always calls the same Agreement, (I did that for testing purposes) but now I want the Agreement which ID is specified in the DataGrid Cell Tag (in this example it's the Months[9].AgreementID).
I can give it to the ViewModel in my Child View's constructor, but I don't think that it's allowed due to the MVVM Pattern (or is it allowed?).
So my question is: How can I pass the AgreementID specified in my ParentView to the ChildView's ViewModel to get the right data for the ChildView?
Ofcourse, more information/code/clarification can be happily provided, just ask :)
Thanks in advance!
Not sure that I got question in righ way but my feelings like you need to use Commands instead of introducing tied coupling by passing back reference to parent itself
Personally, I feel that WPF Views should be nothing more than a pretty reflection of the ViewModel. So the View should not actually be passing any data to the ViewModels - instead it should be reflecting the ViewModel's data.
In your case, I would attach a property to the object that is displayed in each DataGrid Row. For example, if your DataGrid contained Agreement objects, I would ensure that each Agreement object had a property called AgreementDetails which can be viewed from the ToolTip
It's pefectly legal and valid to pass in that ID eitehr via a constructor or through a property. I'm not sure from your code, but if your parent is the one with access to your model, you can also pass teh model into your view model (i.e. via constructor, property, or method).
One thing I often do in this case is to add a property such as the following in the ViewModel of my Parent:
object ActiveItem {get;set;}
I then bind that ActiveItem to the ActiveItem in my grid.
<DataGrid SelectedItem="{Binding ActiveItem}">
</DataGrid>
I have CollectionViewSource in which dates grouped by years and months. Dates are displayed in TreeView (accurately in RadTreeView).
The target is to change selected date without recreating the view (do not call Refresh method).
To do this I implemented IEditableObject in date view model and changed date so:
var selectedDate = SelectedDate;
var editableCollectionView = Dates.View as IEditableCollectionView;
if (null != editableCollectionView && !editableCollectionView.IsEditingItem)
{
editableCollectionView.EditItem(selectedDate);
selectedDate.Date = dt.Date;
editableCollectionView.CommitEdit();
}
But in this case TreeView lost selection and I need to select "selected item" again that leads refreshing data bounded to selected date.
How can I solve this issue? Perfectly using MVVM way.
UPDATE:
If date is alone in group, changing date causes collapsing item which contains it.
UPDATE 2:
May be I shouldn't use SelectedDate property and work only with IsSelected and IsExpanded?
Leverage MVVM for Tree View Item.
Include two writeable properties in your item level class (which serves as the data context to your individual tree view item)
IsExpanded
IsSelected
Have INotifyPropertyChanged implemented and property changed notification raised in the setter of the above two properties.
Now at TreeViewLevel have a Style that binds these properties.
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
....
</...>
This way the expansion and selection is maintained on the tree view as long as it is maintained in the tree view item's data context.
Now remember that expanded states can be true for multiple items but selection state true is applicable for only one item along the entire tree view.
Hope this helps.