WPF: How to use Style.Triggers - wpf

I want to implement (file) Explorer like icon display. The items have date and label.
User should be able to edit the label:
Select an item
Click on label
Label's TextBlock is replaced with TextBox for editing
How to end editing (just for info):
Click anywhere outside of the TextBox
Press Enter keyboard key (by implementing ICommand?)
1st I tried to set the Visibility of TextBlock and TextBox in code found out it is not the 'right' way to to do. Maybe it is possible to edit item's Label using (Data)Triggers?
I can track the OnClickLabelBlock and set selectedMedia.IsEditing = true; but it does not fire the trigger.
Any idea why MediaItem.IsEditing property value change is notifying the DataTrigger? Is it something to do with the order of execution or priority mechanism?
I will pick the answer which guides me to the 'best' architecture to solve it.
Thanks.
XAML:
<Window x:Class="WPFComponents.DailyImages"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Model="clr-namespace:WPFComponents.Model"
Title="Media Items" Height="300" Width="300">
<ListView x:Name="_mediaItemList" ItemsSource="{Binding MediaItems}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled" SelectionMode="Multiple">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate DataType="Model:MediaItem">
<Grid Width="80" Margin="4">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Image HorizontalAlignment="Center" Stretch="Uniform" Source="{Binding Path=IconPath}" Width="70" />
<StackPanel Grid.Row="2">
<TextBlock Text="{Binding Path=Date}" TextWrapping="Wrap" />
<TextBlock x:Name="_labelTextBlock" Text="{Binding Path=Label}" TextWrapping="Wrap"
PreviewMouseLeftButtonDown="OnClickLabelBlock">
</TextBlock>
<TextBox x:Name="_labelTextBox" Text="{Binding Path=Label}" Visibility="Collapsed"
TextWrapping="WrapWithOverflow" TextAlignment="Center">
</TextBox>
</StackPanel>
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsEditing}" Value="True">
<Setter TargetName="_labelTextBlock" Property="Visibility" Value="Collapsed" />
<Setter TargetName="_labelTextBox" Property="Visibility" Value="Visible" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True" VerticalAlignment="Top" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
Source:
public partial class DailyImages
{
public DailyImages()
{
InitializeComponent();
ViewModel.DailyImages dailyImages = new ViewModel.DailyImages();
// DailyImages has ObservableCollection<MediaItem> MediaItems property
_mediaItemList.DataContext = dailyImages;
}
private void OnClickLabelBlock(object sender, MouseButtonEventArgs e)
{
TextBlock notes = sender as TextBlock;
if (notes == null)
return;
MediaItem selectedMedia = notes.DataContext as MediaItem;
if (selectedMedia == null)
{
// TODO: Throw exception
return;
}
_mediaItemList.SelectedItems.Clear();
selectedMedia.IsSelected = true;
selectedMedia.IsEditing = true;
}
public class MediaItem
{
public MediaItem()
{
IsEditing = false;
IsSelected = false;
}
public DateTime Date { get; set; }
public string Label { get; set; }
public string IconPath { get; set; }
public bool IsEditing { get; set; }
public bool IsSelected { get; set; }
}
References:
Dependency Property Value Precedence
Part II: ListView & File Explorer Like Behaviour

MediaItem must implement INotifyPropertyChanged and each of its properties that must be bound, must call RaisePropertyChanged in order for the binding to work correctly. In your case, the Binding on IsEditing has no way to know that the value has changed.

To bind your IsEditing property, WPF has to be notified when it is modified.
Then you have to implement INotifyPropertyChanged in MediaItem. (Or add dependency properties)
public class MediaItem : INotifyPropertyChanged
{
public MediaItem()
{
IsEditing = false;
IsSelected = false;
}
// Use the same pattern for Date, Label & IconPath if these value may change after the MediaItem instance has been added to the collection MediaItems.
public DateTime Date { get; set; }
public string Label { get; set; }
public string IconPath { get; set; }
private bool isSelected;
public bool IsSelected
{
get { return isSelected; }
set
{
if (isSelected != value)
{
isSelected = value;
OnPropertyChanged("IsSelected");
}
}
}
private bool isEditing;
public bool IsEditing
{
get { return isEditing; }
set
{
if (isEditing != value)
{
isEditing = value;
OnPropertyChanged("IsEditing");
}
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
Otherwise, your code is correct.

Related

How to enable a button only when all three textboxes are filled using ICommand in WPF?

I'm new to MVVM. I have three textboxes and a button in my view. I want that button to be enabled when all those three textboxes are filled. My view is as below:
<StackPanel Margin="1,1,1,1" Grid.Row="0">
<!--<Label Margin="2,2,2,2" Content="ID:"/>
<dxe:TextEdit Margin="2,2,2,2" Text="{Binding ElementName=StudentGrid, Path=SelectedItem.Id}"/>-->
<Label Margin="2,2,2,2" Content="Name:"/>
<dxe:TextEdit Margin="2,2,2,2" x:Name="Name" Text="{Binding Path=Name}" />
<Label Margin="2,2,2,2" Content="Last Name:"/>
<dxe:TextEdit Margin="2,2,2,2" x:Name="LastName" Text="{Binding Path=LastName}" />
<Label Margin="2,2,2,2" Content="Age:"/>
<dxe:TextEdit Margin="2,2,2,2" x:Name="Age" Text="{Binding Path=Age}" />
</StackPanel>
<ListView Name="StudentGrid" Grid.Row="1" Margin="1,1,1,1" ItemsSource="{Binding studentList}">
<ListView.View>
<GridView>
<GridViewColumn Header="ID" Width="50" DisplayMemberBinding="{DXBinding Id}"/>
<GridViewColumn Header="Name" Width="80" DisplayMemberBinding="{DXBinding Name}"/>
<GridViewColumn Header="Last Name" Width="80" DisplayMemberBinding="{DXBinding LastName}"/>
<GridViewColumn Header="Age" Width="50" DisplayMemberBinding="{DXBinding Age}"/>
</GridView>
</ListView.View>
</ListView>
<StackPanel Grid.Row="2" Margin="1,2,1,1">
<dx:SimpleButton x:Name="applybtn" Content="Insert" Width="60" HorizontalAlignment="Left" Margin="5,0,0,0" Command="{Binding Path=_myCommand}"/>
</StackPanel>
InserCommand code is:
public class InsertCommand : ICommand
{
public StudentListViewModel _viewModel { get; set; }
public InsertCommand(StudentListViewModel viewModel)
{
_viewModel = viewModel;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_viewModel.InsertStudent();
}
}
StudentListViewModel code is:
public class StudentListViewModel : INotifyPropertyChanged
{
private string _name;
private string _lastName;
private int _age;
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged("Name");
}
}
public string LastName
{ get => _lastName;
set
{
_lastName = value;
OnPropertyChanged("LastName");
}
}
public int Age
{ get => _age;
set
{
_age = value;
OnPropertyChanged("Age");
}
}
public InsertCommand _myCommand { get; set; }
private ObservableCollection<Student> _studentList;
public StudentListViewModel()
{
_myCommand = new InsertCommand(this);
using (MyContext context = new MyContext())
{
_studentList = new ObservableCollection<Student>(context.Students.ToList());
};
}
public void InsertStudent()
{
Student st = new Student()
{
Name = _name,
LastName = _lastName,
Age = _age
};
using (var context = new MyContext())
{
context.Add(st);
context.SaveChanges();
_studentList.Add(st); //For Getting instatnt UI update
}
}
public ObservableCollection<Student> studentList
{
get
{
return _studentList;
}
set
{
_studentList = value;
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
It's not usual to create a new class for each command.
Instead, create a generic command class with action / func parameters to specify each command's logic.
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool> _canExecute;
public RelayCommand(Action execute) : this(execute, () => true)
{
}
public RelayCommand(Action execute, Func<bool> canExecute)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute.Invoke();
public void Execute(object parameter) => _execute.Invoke();
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
Then, create an instance of this class in your ViewModel that you can bind your button to.
public class StudentListViewModel : INotifyPropertyChanged
{
private string _name;
private string _lastName;
private int _age;
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged("Name");
InsertCommand.RaiseCanExecuteChanged();
}
}
public string LastName
{ get => _lastName;
set
{
_lastName = value;
OnPropertyChanged("LastName");
InsertCommand.RaiseCanExecuteChanged();
}
}
public int Age
{ get => _age;
set
{
_age = value;
OnPropertyChanged("Age");
InsertCommand.RaiseCanExecuteChanged();
}
}
public RelayCommand InsertCommand { get; }
public StudentListViewModel()
{
InsertCommand = new RelayCommand(() => InsertStudent(),
() => !string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(LastName) && Age > 0);
...
}
public void InsertStudent()
{
....
}
}
For a cleaner way to handle command refresh, so that each property doesn't need to care how it is used, check out my blog post.
Usually I create Binding Validations so that I can reuse on other textboxes.
Then on the button I create triggers to enable or disable the button based on the validations defined.
<Button Comman="{Binding InsertCommand}" Content="Insert">
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="IsEnabled" Value="False" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ElementName=Name, Path=(Validation.HasError)}" Value="False"/>
<Condition Binding="{Binding ElementName=LastName, Path=(Validation.HasError)}" Value="False"/>
<Condition Binding="{Binding ElementName=Age, Path=(Validation.HasError)}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter Property="IsEnabled" Value="True" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
For very simple not null validations, Instead of creating validations, I just test against the text property of the textbox
<Button Comman="{Binding InsertCommand}" Content="Insert">
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="IsEnabled" Value="True" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ElementName=Name, Path=Text}" Value=""/>
<Condition Binding="{Binding ElementName=LastName, Path=Text}" Value=""/>
<Condition Binding="{Binding ElementName=Age, Path=Text}" Value=""/>
</MultiDataTrigger.Conditions>
<Setter Property="IsEnabled" Value="False" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
button to be enabled when all those three textboxes are filled.
Create a new bool property called IsButtonEnabled.
Whenever text changes (two-way binding and update source triggeer in the binding right? -> How-to bind, my answer) to push the current string(s) to the VM's properties in real time.
Those properties in question, their values will be checked in the IsButtonEnabled getter. Then in each of the setters for the strings, add a OnPropertyChanged for IsButtonEnabled. Also bind IsButtonEnabled to the proper button property to achieve your desired affect.
Example
// Doesn't need its own OnPropertyChanged, because that is set
// for every keystroke.
public bool IsButtonEnabled { get { return !string.IsNullOrEmpty(Name) &&
!string.IsNullOrEmpty(LastName) &&
!string.IsNullOrEmpty(Age); }}
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged("Name");
OnPropertyChanged("IsButtonEnabled");
InsertCommand.RaiseCanExecuteChanged();
}
}
public string LastName
...
set
{
_lastName = value;
OnPropertyChanged("LastName");
OnPropertyChanged("IsButtonEnabled");
...
public string Age
...
set
{
_age= value;
OnPropertyChanged("Age");
OnPropertyChanged("IsButtonEnabled");
....
Xaml
<dx:SimpleButton x:Name="applybtn" IsEnabled="{Binding IsButtonEnabled}" ...
Similar answer given
DataTemplate.DataTrigger to check for greater than or less than?
WPF Multi-Binding / Aggregate Binding to
Collection

In WPF,How to add radio button , checkbox, comboBox based on the type which is configured in the database?

Totally i am new to WPF, I need to solve the problem. Could anybody give me a sample xaml code without using code-behind.
Based on the questType (Check,radio, combo) Control need to be created based on the ObserverableCollection.
public class Order
{
public int OrderCode {get;set;}
public string Description {get;set;}
public ObserverableCollection<question> Questions{get;set;}
}
public class question
{
public string questType {get;set;}
public string Question {get;set;}
public ObserverableCollection<Answer> Answers {get;set;}
}
public class Answer
{
public string Ans{get; set;}
}
Based on the questType (Check,radio, combo)
Control need to be created based on the ObserverableCollection.
example:
1001 Pencil Gender? oMale oFemale oOther
[]Checkbox1 []Checkbox2
1002 Pen Fasting? oYes oNo
Here is how I would do it:
Code behind:
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Windows;
namespace Ans
{
public class Order
{
public int OrderCode { get; set; }
public string Description { get; set; }
public ObservableCollection<Question> Questions { get; set; }
}
public class Question
{
public string questType { get; set; }
public string Label { get; set; }
public ObservableCollection<Answer> Answers { get; set; }
}
public class Answer
{
public string Ans { get; set; }
public bool IsSelected { get; set; }
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
#region Order
/// <summary>
/// Order Dependency Property
/// </summary>
public static readonly DependencyProperty OrderProperty =
DependencyProperty.Register("Order", typeof(Order), typeof(MainWindow),
new FrameworkPropertyMetadata((Order)null));
/// <summary>
/// Gets or sets the Order property. This dependency property
/// indicates ....
/// </summary>
public Order Order
{
get { return (Order)GetValue(OrderProperty); }
set { SetValue(OrderProperty, value); }
}
#endregion
public MainWindow()
{
InitializeComponent();
Order = new Order()
{
Questions = new ObservableCollection<Question>()
{
new Question()
{
questType = "Combo",
Label = "Combo",
Answers = new ObservableCollection<Answer>()
{
new Answer(){Ans = "Female"},
new Answer(){Ans = "Male"}
}
},
new Question()
{
questType = "Check",
Label = "Multi",
Answers = new ObservableCollection<Answer>()
{
new Answer(){Ans = "Female"},
new Answer(){Ans = "Male"}
}
},
new Question()
{
questType = "Radio",
Label = "Radio",
Answers = new ObservableCollection<Answer>()
{
new Answer(){Ans = "Female"},
new Answer(){Ans = "Male"}
}
}
}
};
DataContext = this;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
foreach(Question q in Order.Questions)
{
Console.WriteLine( q.Label + " : " + string.Join(", " , q.Answers.Where(a=>a.IsSelected).Select(a=>a.Ans)) );
}
}
}
}
XAML:
<Window x:Class="Ans.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:Ans"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<DataTemplate x:Key="ComboQuestion">
<ComboBox ItemsSource="{Binding Answers}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Ans}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
<ComboBox.ItemContainerStyle>
<Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
</DataTemplate>
<DataTemplate x:Key="CheckQuestion">
<ItemsControl ItemsSource="{Binding Answers}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Ans}" IsChecked="{Binding IsSelected, Mode=TwoWay}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
<DataTemplate x:Key="RadioQuestion">
<ItemsControl ItemsSource="{Binding Answers}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<RadioButton Content="{Binding Ans}" IsChecked="{Binding IsSelected, Mode=TwoWay}" GroupName="{Binding DataContext.Label, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</Window.Resources>
<Grid>
<ItemsControl ItemsSource="{Binding Order.Questions}" Grid.IsSharedSizeScope="True">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Label"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Label}"/>
<ContentControl x:Name="ccQuestion" Grid.Column="1" Content="{Binding}" Margin="10"/>
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding questType}" Value="Combo">
<Setter TargetName="ccQuestion" Property="ContentTemplate" Value="{StaticResource ComboQuestion}"/>
</DataTrigger>
<DataTrigger Binding="{Binding questType}" Value="Check">
<Setter TargetName="ccQuestion" Property="ContentTemplate" Value="{StaticResource CheckQuestion}"/>
</DataTrigger>
<DataTrigger Binding="{Binding questType}" Value="Radio">
<Setter TargetName="ccQuestion" Property="ContentTemplate" Value="{StaticResource RadioQuestion}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="Order" Click="Button_Click" VerticalAlignment="Bottom"/>
</Grid>
</Window>
The only thing I've added to your model is IsSelected property that allows to know if this answer was selected.
The other important thing is the Radios. Their GroupName property defines the scope. So if no GroupName is set then when clicking on a radio in one question it will unselect radio in another question. I used Question label in my solution however it only works if labels are unique.
Another point is that data triggers are OK if you have 3-5 question types and if thay are only based on the questionType. However for more complex scenarios you can look for ItemTemplateSelector. It allows to write C# code that will select the template based on each item in ItemsControl.

WPF MVVM IDataErrorInfo

i have an app in MVVM pattern ; it contains one textbox and one button;
i add a validation that if textbox is empty , textbox color change to red;
and at this point i want that the button enable be false till the user
enter character in textbox and then button enable change to true;
my XAML Code is:
<Window.Resources>
<ControlTemplate x:Key="ErrorTemplate">
<DockPanel LastChildFill="True">
<Border BorderBrush="Pink" BorderThickness="1">
<AdornedElementPlaceholder />
</Border>
</DockPanel>
</ControlTemplate>
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBox Margin="10" Text="{Binding ValidateInputText,UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, ValidatesOnDataErrors=True, NotifyOnValidationError=True, ValidatesOnExceptions=True}"
Validation.ErrorTemplate="{StaticResource ErrorTemplate}">
</TextBox>
<Button Margin="10" Grid.Row="2" Command="{Binding ValidateInputCommand}"/>
and my command class is:
public class RelayCommand : ICommand
{
private Action WhatToExcute;
private Func<bool> WhenToExecute;
public RelayCommand(Action what,Func<bool>when )
{
WhatToExcute = what;
WhenToExecute = when;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return WhenToExecute();
}
public void Execute(object parameter)
{
WhatToExcute();
}
}
and my viewmodel is:
public class ViewModel : IDataErrorInfo , INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
Product product = new Product();
public ViewModel()
{
ValidateInputCommand = new RelayCommand(action, valid);
}
public void action()
{
}
public bool valid()
{
if (product.Name == null)
return false;
return true;
}
public string ValidateInputText
{
get { return product.Name; }
set {
product.Name = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(ValidateInputText));
}
}
}
public RelayCommand ValidateInputCommand { get; set; }
public string this[string columnName]
{
get
{
if ("ValidateInputText" == columnName)
{
if (String.IsNullOrEmpty(ValidateInputText))
{
return "Please enter a Name";
}
}
return "";
}
}
public string Error
{
get
{
throw new NotImplementedException();
}
}
}
now when i run my app , the color of text box is red and when i enter a
character it change to normal that is ok but the
button enable is false and does not change
so i could not click button.
what should i do?
The CanExecuteChanged event on your RelayCommand never gets fired. Therefore the button to which the command has been bound will never re-evaluate its IsEnabled status. Your view model could notify the command on changes in its valid status and the command should in turn raise its CanExecuteChanged event.

Unable to fire TreeView properties events

I am having problem with the following code. I have a TreeView Control which is bound to a Collection. The TreeView does get populated with the desired results. HOwever the "IsSelected" property and ContextMenu's click Command is not firing. Following is the XAML code.
<UserControl x:Class="Plan.Views.PadView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:v="clr-namespace:Planner.Views"
xmlns:vm="clr-namespace:Planner.ViewModels"
<Grid>
<StackPanel Orientation="Vertical">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Horizontal" OpacityMask="#FFECF5F5">
<TreeView ItemsSource="{Binding Pads}" Name="tree_View" Width="190">
<TreeView.ItemContainerStyle >
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding WellPadViewModel.IsSelected}" />
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Header="Rename" Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type TreeView}}, Path=DataContext.RenameCommand}" />
</ContextMenu>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle >
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Members}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" >
<TextBlock.InputBindings>
<KeyBinding Key="F2" Command="{Binding RenameCommand}"/>
</TextBlock.InputBindings>
</TextBlock>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</StackPanel>
</Grid>
</StackPanel>
</Grid>
</UserControl>
And here is my ViewModel
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Input;
using System.ComponentModel;
using WPFApplication;
namespace FieldPlanner.ViewModels
{
public class PlanViewModel : BaseViewModel
{
Collection<Pads> pads = new Collection<Pads>();
public PlanViewModel()
{
IsSelected = true;
pads = new Collection<Pad>();
}
private ICommand _RenameCommand;
public ICommand RenameCommand
{
get
{
if (_RenameCommand == null)
{
_RenameCommand = new RelayCommand1((o) =>
{
// Your logic should go here
MessageBox.Show("Please rename me");
});
}
return _RenameCommand;
}
}
public ObservableCollection<PadInfo> Members { get; set; }
private static object _selectedItem = null;
// This is public get-only here but you could implement a public setter which also selects the item.
// Also this should be moved to an instance property on a VM for the whole tree, otherwise there will be conflicts for more than one tree.
public static object SelectedItem
{
get { return _selectedItem; }
private set
{
if (_selectedItem != value)
{
_selectedItem = value;
OnSelectedItemChanged();
}
}
}
public static void OnSelectedItemChanged()
{
// Raise event / do other things
}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected != value)
{
_isSelected = value;
OnPropertyChanged("IsSelected");
if (_isSelected)
{
SelectedItem = this;
}
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
var handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
/// <summary>
/// Class to hold the Pads info for a tree
/// </summary>
public class Pad
{
/// <summary>
/// Default Constructor
/// </summary>
public Pad()
{
this.Members = new ObservableCollection<PadInfo>();
}
/// <summary>
/// Name of the pad
/// </summary>
public string Name { get; set; }
/// <summary>
/// Members of the pad
/// </summary>
public ObservableCollection<PadInfo> Members { get; set; }
}
/// <summary>
/// Class to hold the well and slot IDs snapped to a pad
/// </summary>
public class PadInfo
{
/// <summary>
/// Slot ID
/// </summary>
public string SlotID { get; set; }
/// <summary>
/// Well ID
/// </summary>
public string WellID { get; set; }
}
public class RelayCommand1 : ICommand
{
#region Fields
readonly Action<object> _execute;
readonly Predicate<object> _canExecute;
#endregion // Fields
#region Constructors
public RelayCommand1(Action<object> execute)
: this(execute, null)
{
}
public RelayCommand1(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
#endregion // Constructors
#region ICommand Members
// [DebuggerStepThrough]
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
_execute(parameter);
}
#endregion // ICommand Members
}
}
How can I identify the issue?
You have two problems:
IsSelected:
<Setter Property="IsSelected" Value="{Binding WellPadViewModel.IsSelected}" />
In TreeViewItem DataContext is set to instance of Pad and Pad doesn't have property IsSelected You have to do sth like this:
<Setter Property="IsSelected" Value="{Binding DataContext.IsSelected, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}" />
Problem with ContextMenu is much more sirious. ContextMenu isn't in VisualTree so you cannot bind to RelativeSource. Solution is here WPF Relative source- Cannot find source for binding with reference
Best regards
Please set the Tag property in your DataTemplate to TreeViewItem. I have sth like this:
<DataTemplate>
<Grid Width="270" Height="20" Tag="{Binding DataContext, RelativeSource = {RelativeSource AncestorType={x:Type UserControl}}}">
...
<Grid.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Edit">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<command:EventToCommand Command="{Binding Tag.YOURCOMMAND}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</MenuItem>
</ContextMenu>
</Grid.ContextMenu>
</Grid>
</DataTemplate>
It should work.

WPF datagrid with ToggleButton binding

I'm relatively new to WPF and am having a hard time binding a custom class to a datagrid. While the text properties are OK (they are read-only anyway), the togglebuttons for the boolean properties are not updated in my item list, and they are also not displayed according to the values set initially. They do however respond correctly to clicks in the UI.
<Style x:Key="ToggleImageStyleBien" TargetType="ToggleButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<StackPanel>
<Image Name="img" Source="Images/transp.png"/>
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="ToggleButton.IsChecked" Value="True">
<Setter TargetName="img" Property="Source" Value="Images/good.png"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Here's the DataGrid itself:
<DataGrid AutoGenerateColumns="False" Height="Auto" Name="dataGridRevision" Width="Auto" Margin="6,6,6,0" ItemsSource="{Binding}" VerticalScrollBarVisibility="Auto" VerticalGridLinesBrush="{x:Null}">
<DataGrid.Columns>
<DataGridTextColumn Header="Code" Binding="{Binding Code}" Visibility="Collapsed"/>
<DataGridTextColumn Header="DescripciĆ³n" Binding="{Binding Description}" IsReadOnly="True"/>
<DataGridTemplateColumn Header="Bien">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ToggleButton Style="{StaticResource ToggleImageStyleBien}" Click="ToggleButton_Click" IsChecked="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}, Path=ReviewItem.Good, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Comentario" />
</DataGrid.Columns>
</DataGrid>
This is the class I'm binding to:
public class ReviewItem
{
public string Code { set; get; }
public string Description { set; get; }
public bool Good { set; get; }
public bool Bad { set; get; }
public string Comment { set; get; }
}
As far as I can tell, I'm not using the right Binding property in the ToggleButton, but I have tried a lot and have run out of ideas. The list properties are not changing on click, the values are not displayed according to the data.
Please help...
Thanks!
Joerg.
Changed the class to this, based on other examples found:
public class ReviewItem : INotifyPropertyChanged
{
public string Code { set; get; }
public string Description { set; get; }
public bool Bad { set; get; }
public string Comment { set; get; }
private bool _isChecked;
public bool Good
{
get { return _isChecked; }
set
{
System.Diagnostics.Debug.WriteLine("Good = " + value);
_isChecked = value;
OnPropertyChanged("Good");
}
}
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
You are currently binding to UserControl.ReviewItem, which is not a valid property on a UserControl.
Try binding to DataContext.ReviewItem instead which will bind to UserControl.DataContext.ReviewItem
If you do not mean to bind to UserControl.DataContext.ReviewItem.Good, and instead want to bind to DataGrid.ReviewItems[x].Good, then you only need to bind IsChecked="{Binding Good}". This is because the default DataContext on a DataGridCell is the row's data item, so if your collection contains a list of ReviewItems, the DataContext of the cell is already set to a ReviewItem
Also, I would highly recommend using Snoop for debugging WPF bindings. You can use it on your application to see what the DataContext is for individual UI Elements, and figure out if your bindings are correct or not.

Resources