WPF binding based on comparison - wpf

There is a relatively simple thing I'm trying to achieve but I'm unsure how to do it. Basically, I have a CLR class as follows:
class SomeClass
{
public SomeEnum Status;
}
public enum SomeEnum { One, Two, Three };
I've got a DataGrid that I'm binding an ObservableCollection<SomeClass> programmatically through the code-behind. In this DataGrid I have a DataGridTemplateColumn containing two buttons, as follows:
<toolkit:DataGridTemplateColumn Header="Actions">
<toolkit:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Button Content="ActionOne" />
<Button Content="ActionTwo" />
</StackPanel>
</DataTemplate>
</toolkit:DataGridTemplateColumn.CellTemplate>
</toolkit:DataGridTemplateColumn>
What I want to do is bind the IsEnabled property of these buttons to a comparison based on the value of {Binding Path=Status}. For example, in pseudocode:
ActionOne.IsEnabled = BoundValue.Status != SomeEnum.Two
ActionTwo.IsEnabled = BoundValue.Status == SomeEnum.One || BoundValue.Status == SomeEnum.Two
Is there anyway to do this in XAML? The alternative would be just to write a value converter for each button, but since the content and other details of the button may vary, too, I don't want to end up writing like 6 value converters.
Cheers!

Why not expose additional Properties in SomeClass that performs the comparison logic?
ex:
public bool ActionOneEnabled
{
get { return Status != SomeEnum.Two; }
}
Then you can easily bind the Button's IsEnabled to the appropriate Property.
Don't forget to include an OnPropertyChanged("ActionOneEnabled") in your setter for Status - so that when your Status changes your Properties based on Status are re-evaluated.

You could do this using a DataTrigger in conjunction with a Converter like below. However, Bryan's solution has the benefit of not using multiple converters and it looks like that was one of your concerns so his answer might be better for your scenario.
<Button>
....
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="IsEnabled" Value="False" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Status, Converter={StaticResource yourConverter}}" Value="True">
<Setter Property="IsEnabled" Value="True" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
Another option would be to not use the DataTrigger and add the binding directly in the IsEnabled property:
<Button
IsEnabled="{Binding Path=Status, Converter={StaticResource yourConverter}}"
...
/>

Related

WPF MVVM Light - About views communicating

I am currently working on a project for work. I am seeking an outside design opinion, as well as some general information on the issue I am faced with.
We have a MainWindow.xaml file that is located in the root directory of the project. In this main window is some design and logic for some collapsing stack panels, ribbon toolbar, etc.
So far the idea is to include a different in each stack panel to help make the code neat. The views are located in a 'Views' folder. So just to be clear, the MainWindow.xaml and other views ARE NOT in the same directory. This is open to change, if necessary.
So here is my question/issue: We have a Window ('A'), a main panel with a collapsable stack panel with some information ('B') that is contained in window 'A'. Then there is another stack panel to manage the contents in 'B', (collapse/visisble) ('C').
'A' contains a toggle button to show/collapse 'B'.
'B' contains a button to show/collapse 'C'.
'C' contains a button to show/collapse itself, 'C'.
'C' should have its logic all contain within a view, so the MainWindow ('A') should have a simple tag:
<StackPanel Style="{StaticResource FrameGradient}" Tag="{Binding ElementName=ToggleButton}">
<view:Content></view:Content>
</StackPanel>
Currently, the bindings for toggling the buttons within 'A' are in the styling. The In this case FrameGradient has triggers like so:
<Style x:Key="FrameGradient" TargetType="{x:Type StackPanel}">
//Setter properties
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Tag.IsChecked, RelativeSource={RelativeSource Self}}" Value="False">
<Setter Property="StackPanel.Visibility" Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Tag.IsChecked, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="StackPanel.Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
Is it possible to, within the 'Content' View to TOGGLE the panel, 'C', which is NOT within the view? I feel like I am missing a core idea of XAML here. I found a 'cheap' work around which is to place the 'Close' button from the Content View outside of the tags, but then that leads to styling issues and I feel like I shouldn't have to do something silly like that. Again, the idea is that the toggle button for Stack Panel 'C' is contained within another view and I want to be able to toggle it from another view.
I apologize if I am not clear enough, I will provide whoever asks with more information if required here.
UPDATE
I have some time to actually add the code I am using so that this might make more sense.
MainWindow.xaml - Logic for Filter panel (Located in root)
<StackPanel Grid.Row="1" Grid.Column="4" Visibility="Collapsed" Style="{StaticResource FrameGradient}">
<Grid x:Name="FilterContentGrid">
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<view:Filters></view:Filters>
</Grid>
</StackPanel>
Filters.xaml - Logic for Filters view (Located in /Views)
The button within the file that needs to Collapse the above StackPanel.
<Button x:Name="FilterManagementCloseButton" Content="CLOSE"></Button>
Theme.Xaml - Logic for all styling (Located in root, along with MainWindow.xaml and App.xaml)
Button Styling
<Style x:Key="FilterManagementCloseButton" TargetType="Button">
<Setter Property="Padding" Value="10,5,20,3" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Tag.IsChecked, RelativeSource={RelativeSource AncestorType={x:Type Local:MainWindow}}}" Value="True">
<Setter Property="StackPanel.Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
And finally, the FrameGradient Styling also located in Theme.xaml
<Style x:Key="FrameGradient" TargetType="{x:Type StackPanel}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Tag.IsChecked, RelativeSource={RelativeSource Self}}" Value="False">
<Setter Property="StackPanel.Visibility" Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Tag.IsChecked, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="StackPanel.Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
SO, I hope this makes things more clear. I want the CLOSE button within Filters.xaml to COLLAPSE the stackpanel that is located in MainWindow. I realize this code is a mess at the moment.
Is it possible to, within the 'Content' View to TOGGLE the panel, 'C',
which is NOT within the view?
Create a shared VM which each other VM will have a property for which it can access. This VM can be loaded with during initialization of the other VMs. To allow for changes to happen put INotifyProperty(ies) on the shared VM which will then flag the desired logic across all views. Finally bind the target control(s) to your datacontext as normal except sub path into the shared VM target's property.
Hence when one view toggles (two way binding) a shared property it is reflected on the view of the target panel.
Update Example
The idea here is that one creates a viewmodel for the AppPage. That VM will hold generic flags which are shared across all viewmodels. Each subsequently created ViewModel will have a reference to the AppPage's viewmodel.
The example below is a mainpage where the AppVM contains a flag which informs the mainpage whether a login is in process. If it is and that value is true then a bound button on the mainpage will be enabled.
Subsequently the mainpage can override the appvm and put a new value within that flag by a bounded checkbox that can in-directly change whether the button is enabled; thus changing the flag for all other VMs in the process.
Here is the Mainpage VM, for this example I simply create the AppVM, but it could be passed in, or gotten from a static reference elsewhere. NOTE also how I don't care when AVs (appVM) property changes; it is not required for this example (we are not binding anything to AppVM, just its properties which need to be monitored).
public class MainVM : INotifyPropertyChanged
{
public AppVM AV { get; set; }
public MainVM()
{
AV = new AppVM() { LoginInProcess = true };
}
}
Here is the AppVm
public class AppVM : INotifyPropertyChanged
{
private bool _LoginInProcess;
public bool LoginInProcess
{
get { return _LoginInProcess; }
set { _LoginInProcess = value; OnPropertyChanged(); }
}
}
Here is MainPage's Xaml where the datacontext has been set to an instance of MainVM:
<StackPanel Orientation="Vertical">
<CheckBox Content="Override"
IsChecked="{Binding AV.LoginInProcess, Mode=TwoWay}"/>
<Button Content="Login"
IsEnabled="{Binding AV.LoginInProcess}"
Width="75" />
</StackPanel>
I base the MVVM off of my blog article Xaml: ViewModel Main Page Instantiation and Loading Strategy for Easier Binding which explains the other missing items of this example such as the mainpage's datacontext loading.
You can use RelativeSource Bindings to bind from child views to properties in parent view models. Let's say you have a ToggleButton in MainWindow.xaml that is data bound to a property named IsChecked, which is declared in the object that is data bound to the MainWindow DataContext property. You could data bind to that same property from any child view with a RelativeSource Binding, like this:
<Style x:Key="FrameGradient" TargetType="{x:Type StackPanel}">
<Setter Property="StackPanel.Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=DataContext.IsChecked, RelativeSource={
RelativeSource AncestorType={x:Type Local:MainWindow}}}" Value="False">
<Setter Property="StackPanel.Visibility" Value="Collapsed" />
</DataTrigger>
<!-- Note that there is no need for two Triggers here -->
<!-- One Setter and one Trigger is enough -->
</Style.Triggers>
</Style>

DataTrigger not working (LineCount always -1)

The datatrigger below is not working. Any idea why not?
I checked in code behind that the LineCount in two situations is 1 respectively 4.
But when I change the Value to "-1" the trigger is working.
So why is the LineCount always -1?
<TextBox x:Name="TextInfo" TextWrapping="Wrap" Text="Information" HorizontalAlignment="Stretch" Foreground="OrangeRed">
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding LineCount, ElementName=TextInfo}" Value="4">
<Setter Property="Background" Value="Green" />
</DataTrigger>
<DataTrigger Binding="{Binding LineCount, ElementName=TextInfo}" Value="1">
<Setter Property="Background" Value="PowderBlue" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
Looking at the TextBox.LineCount Property page on MSDN, we can see that it is not a DependencyProperty. Furthermore, as the TextBox class does not implement the INotifyPropertyChanged interface, I can only imagine that the value of this property will never update for you in a Binding and can only be used in code.
The only way that I can see you using this property is if you create an Attached Property to access it and expose the updated value.
A quick decompile of the TextBox code shows that LineCount is not a DependencyProperty. So when the value is updated it will not update your trigger.
This is LineCount property from the System.Windows.Controls.TextBox class in C#. You can see here that there is not DependencyProperty backing the property.
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public int LineCount
{
get
{
if (this.RenderScope == null)
return -1;
else
return this.GetLineIndexFromCharacterIndex(this.TextContainer.SymbolCount) + 1;
}
}
The answer to this question as a decent solution for creating an attached property that will observe when the text in the TextBox is modified, and give the current line position. You can change your binding to listen for the attached property.
You dont need ElementName, use RelativeSource:
<DataTrigger Binding="{Binding LineCount, RelativeSource={RelativeSource Self}}" Value="4">
The LineCount property of a TextBox can be used to retrieve the current number of lines of text in a TextBox. If the text wraps to multiple lines, this property will reflect the visible number of wrapped lines that the user sees.
<TextBox x:Name="TextInfo" Text="Information" HorizontalAlignment="Stretch" Foreground="OrangeRed" **TextWrapping="Wrap"**>
<TextBox.Style>
....
</TextBox.Style>
</TextBox>

DataTrigger Overflow exception

How can this be an overflow exception...?
<DataTemplate x:Key="ElementTemplate">
<StackPanel Orientation="Horizontal">
<StackPanel.Style>
<Style TargetType="{x:Type StackPanel}">
<Style.Triggers>
<DataTrigger Binding="{Binding Converter={StaticResource TypeConv}}" Value="{x:Type models:GroupModel}">
<Setter Property="Margin" Value="5 0 0 0"></Setter>
<Setter Property="DataContext" Value="{Binding Model}"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBlock Text="{Binding Name}"></TextBlock>
...
<StackPanel/>
<DataTemplate/>
For the reason: this is a template (with at least 25 UI contros) that normally needs Model A as datatype. the DataTemplate is a ListvVew ItemTemplate. But the datatype can be of type Model B. Model B has a property called 'Model', which is of type Model A.
So instead of copy pasting the whole block template and use style triggers or DataTemplate selectors, I just want to change the DataContext (from "{Binding}" to "{Binding Model}")
anyone has some suggestions, a solution?
Thx!
EDIT: the Converter returns the type of the incoming value (the data object itself). that way i can know when Model B is using the template and so to change the DataContext.
A work-around I would suggest to avoid possible recursion between setting the data context and triggering the DataTrigger:
Have both ModelA and ModelB implement a common interface called IListViewModel for example with a single property getter:
public interface IListViewModel
{
ModelA Model {get;}
}
Then, ModelA's implementation will return this, while ModelB's implementation returns this.ModelA The DataTemplate simply binds to the .Modelof whichever view model it's given.

Set parent controls property in xaml

I have a panel with a custom ListView. The ListView's items contains a GroupBox. The GroupBox contains a ListView. This ListView's items contains a GroupBox and so on.
All of the controls above have custom templates and styles
There are ToggleButtons in the Controls VisualTree lowest nodes.
When these buttons are checked I need to disable all the panel except of the button was clicked.
I would like to avoid event chaining through the parents in viewModel classes.
I'm using mvvm pattern and I would like to solve it in the xaml side if its possible.
EDIT: Here is a screenshot and the Pick button should disable the panel
Any suggestions are warmly welcomed.
you need to implement the relative source binding something like below.
IsEnabled="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}},Path=IsEnabled}"
Just have a read-only property in your ViewModel that is the negation of the property that your ToggleButton is bound to.
ViewModel:
private Boolean mSourceIsPicked;
public Boolean SourceIsPicked
{
get { return mSourceIsPicked; }
set
{
SetProperty("SourceIsPicked", ref mSourceIsPicked, value);
NotifyPropertyChanged("IsSourceChangeable");
}
}
public Boolean IsSourceChangeable
{
get { return ! this.SourceIsPicked; }
}
Then, in your View, just bind the IsEnabled property of the other controls to that new property.
<ComboBox ItemsSource="{Binding SourceTypes}"
IsEnabled={Binding IsSourceChangeable}" />
The advantage of binding to a property is that you can add/remove controls in your view and just bind to this property without changing additional XAML. You can also change the behavior of any control by not binding to this property.
If you really want a XAML-only solution, you can name each of the controls in the panel, and use a DataTrigger using TargetName on the "SourceIsPicked" property to disable the others:
<ComboBox x:Name="cboSourceTypes" ... />
<ComboBox x:Name="cboSourceNames" ... />
<ToggleButton>
<ToggleButton.Style>
<Style TargetType="{x:Type ToggleButton}">
<Style.Triggers>
<DataTrigger Binding="{Binding SourceIsPicked}" Value="True">
<Setter TargetName="cboSourceTypes"
Property="IsEnabled"
Value="False" />
<Setter TargetName="cboSourceNames"
Property="IsEnabled"
Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</ToggleButton.Style>
</ToggleButton>
Note that this is all freehand, so you may need to adjust it a bit, but it gives you the idea.

Bind two elements' Visibility to one property

I have two Menu Item elements - "Undelete" and "Delete" who have complementary visibility: when one is shown, the other one is hidden.
In the code of the ViewModel I have a dependency property FilesSelectedCanBeUndeleted defined as below:
private bool _filesSelectedCanBeUndeleted;
public bool FilesSelectedCanBeUndeleted
{
get
{
return _filesSelectedCanBeUndeleted;
}
set
{
_filesSelectedCanBeUndeleted = value;
OnPropertyChanged("FilesSelectedCanBeUndeleted");
}
}
the XAML for the Undelete button looks like below:
<MenuItem Header="Undelete" Command="{Binding UndeleteCommand }"
Visibility="{Binding Path=FilesSelectedCanBeUndeleted,
Converter={StaticResource BoolToVisConverter}}" >
As you can see the Visibility of the Undelete is bind to the FilesSelectedCanBeUndeleted
property ( with the help of a BooleanToVisibilityConveter).
Now my question is, how can I write the XAML to bind the Visibility of the Delete button to the "NOT" value of the FilesSelectedCanBeUndeleted property?
Thanks,
Here is an example of a custom IValueConverter, that allows you to reverse the visibility logic. Basically, one MenuItem will be visible when your view-model property is true, and the other would be collapsed.
So you'd need to define two instances of the converter like so:
<local:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<local:BooleanToVisibilityConverter x:Key="ReversedBooleanToVisibilityConverter" IsReversed="true" />
You can use apply the datatrigger to you menuitem to avoid another property in your viemodel like this -
<MenuItem Header="Delete"
Command="{Binding DeleteCommand }">
<MenuItem.Style>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding FilesSelectedCanBeUndeleted}" Value="False">
<Setter Property="Visibility"
Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</MenuItem.Style>
</MenuItem>
Create new property on your ViewModel and just Negate 'FilesSelectedCanBeUndeleted' and then bind to it.
I did something like this a while ago with a simple negation...
private bool _filesSelectedCanBeUndeleted;
public bool FilesSelectedCanBeUndeleted{
get{
return _filesSelectedCanBeUndeleted;
}
set{
_filesSelectedCanBeUndeleted = value;
OnPropertyChanged("FilesSelectedCanBeUndeleted");
// You have also to notify that the second Prop will change
OnPropertyChanged("FilesSelectedCanBeDeleted");
}}
public bool FilesSelectedCanBeDeleted{
get{
return !FilesSelectedCanBeUndeleted;
}
}
Xaml could look like this then ....
<MenuItem Header="Delete"
Command="{Binding DeleteCommand }"
Visibility="{Binding Path=FilesSelectedCanBeDeleted, Converter={StaticResource BoolToVisConverter}}" >

Resources