Change the visibility of elements in ComboBox dynamically using MVVM framework - c#

Ok,
I have seen a few similar questions but have not been able to figure out this problem for the past couple days. I have two Comboboxes and I want each one to hide the selected element in the other one. For example, if I select a value in ComboBox 1 that selected item should be removed as an option in ComboBox 2.
I thought about using a command but ComboBoxes don't have commands. I have pasted below the comboboxes' XAML and ViewModel code. I would appreciate any help with this. I know the code below is wrong but I think that the logic for this should be in the setters of the bounded to ItemSource.
<ComboBox Margin="0,7,0,0"
Name="ComboBoxA"
HorizontalAlignment="Stretch"
Header="{Binding AccountHeader}"
ItemTemplate="{StaticResource ComboBoxTemplate}"
ItemsSource="{Binding ChargedAccounts,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding SelectedAccount,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />
<ComboBox x:Uid="TargetAccountTextBox"
Name="ComboBoxB"
Margin="0,7,0,0"
HorizontalAlignment="Stretch"
Header="target account"
ItemTemplate="{StaticResource ComboBoxTemplate}"
ItemsSource="{Binding TargetAccounts,
Mode=TwoWay,
namespace MoneyFox.Shared.ViewModels
{
[ImplementPropertyChanged]
public class ModifyPaymentViewModel : BaseViewModel, IDisposable
{
private readonly IDefaultManager defaultManager;
private readonly IDialogService dialogService;
private readonly IPaymentManager paymentManager;
//this token ensures that we will be notified when a message is sent.
private readonly MvxSubscriptionToken token;
private readonly IUnitOfWork unitOfWork;
// This has to be static in order to keep the value even if you leave the page to select a category.
private double amount;
private Payment selectedPayment;
public ModifyPaymentViewModel(IUnitOfWork unitOfWork,
IDialogService dialogService,
IPaymentManager paymentManager,
IDefaultManager defaultManager)
{
this.unitOfWork = unitOfWork;
this.dialogService = dialogService;
this.paymentManager = paymentManager;
this.defaultManager = defaultManager;
TargetAccounts = unitOfWork.AccountRepository.Data;
ChargedAccounts = unitOfWork.AccountRepository.Data;
token = MessageHub.Subscribe<CategorySelectedMessage>(ReceiveMessage);
}
ObservableCollection<Account> _SelectedAccount;
ObservableCollection<Account> SelectedAccount
{
get
{
return _SelectedAccount;
}
set
{
_SelectedAccount = value;
for(int i = 0; i < ChargedAccounts.Count; i++)
{
if(ChargedAccounts[i].ToString() == _SelectedAccount.ToString())
{
ChargedAccounts.Remove(ChargedAccounts[i]);
}
}
}
}
ObservableCollection<Account> _TargetAccount;
ObservableCollection<Account> Targetccount
{
get
{
return _SelectedAccount;
}
set
{
_SelectedAccount = value;
for (int i = 0; i < TargetAccounts.Count; i++)
{
if (TargetAccounts[i].ToString() == _SelectedAccount.ToString())
{
TargetAccounts.Remove(ChargedAccounts[i]);
}
}
}
}

While I do agree with a lot of the points in the answer provided by Ed, there is a simpler way to do this without DataTriggers or Converters. There is already a filterable CollectionViewSource in the framework that is your friend (Scott Hanselman loves it)
I would bind ComboBoxA to your regular ChargedAccounts property, but I would modify ComboBoxB to:
bind to a property in the code behind of the View that returns a ICollectionView
in a SelectionChanged event handler for ComboBoxA (also in the code behind of the view) I would adjust the filter for the ICollectionView to exclude the currently selected item
Roughly, this can be done in just a couple of lines:
public ICollectionView FilteredData { get; set; }
private void ComboBoxA_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var z = new CollectionViewSource {Source = ViewModel.ChargedAccounts.Where(p => p != ViewModel.SelectedAccount) };
FilteredData = z.View;
}
Of course this assumes you've done the right thing by having a ViewModel property in the code behind of your view preferably exposed as an interface, and that the ChargedAccounts and SelectedAccount properties are available via that interface.
You could also cobble these couple of lines together in your viewmodel and trigger it via a property change on SelectedAccount - I just have the opinion that a filter operation in response to a UI action should go in the code behind of the UI, but that decision is really up to you.

Give the comboboxes an ItemContainerStyle (TargetType="ComboBoxItem") with a data trigger. For ComboBoxA, that'll look like this:
<ComboBox
...
x:Name="ComboBoxA"
...
>
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding
Converter="{local:ObjectEquals}"
>
<Binding
Path="SelectedItem"
ElementName="ComboBoxB" />
<!-- Binding with no properties just binds to the DataContext -->
<Binding />
</MultiBinding>
</DataTrigger.Binding>
<Setter
Property="Visibility"
Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
ComboBoxB gets the same deal, but ElementName="ComboBoxA" in the SelectedItem binding.
And we'll need to write that multi-value converter. It's as easy as they come:
public class ObjectEquals : MarkupExtension, IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values.Length == 2 && values[0] == values[1];
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
It'd be so convenient if you could bind DataTrigger.Value to {Binding}, but it's not a dependency property.
You *could^ also do this purely in the viewmodel by temporarily removing SelectedAccount from TargetAccounts -- you'd have a private full _targetAccountsFull list, and a public filtered one. The setter for SelectedAccount would filter the list. Were you trying to do that already?
But that's not my idea of a good solution. Hiding combo box items is UI design stuff; the viewmodel shouldn't be involved in it, and in fact shouldn't even be aware that such things take place. One of the pleasures of WPF/MVVM is that you can separate that stuff out into pure UI code in the view. The viewmodel has its own complexities to worry about.
By the way, you bind SelectedItem to SelectedAccount, but SelectedAccount is an ObservableCollection. That makes no sense. There's one selected account. Make it a single Account, not a collection of them.

Related

UWP - How to change ItemsControl item's DataTemplate as an item's property changes?

I have an ItemsControl bound to an ItemsSource. Each item can have one of several DataTemplates assigned depending on the value of various properties on the item. These properties can change at runtime, and the DataTemplates need to be swapped out individually. In WPF I was able to do so with the following (partial simplified xaml):
<ItemsControl
ItemsSource="{Binding Items}">
<ItemsControl.ItemContainerStyle>
<Style TargetType="{x:Type ContentPresenter}">
<Setter Property="ContentTemplate">
<Setter.Value>
<MultiBinding Converter="{StaticResource RowTemplateConverter}">
<Binding Path="(local:Row.Sum)" />
<Binding Path="(local:Row.Avg)" />
<Binding Path="(local:Row.ShowFlagA)" />
<Binding Path="(local:Row.ShowFlagB)" />
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</ItemsControl.ItemContainerStyle>
I've run into several issues trying to move this to UWP:
MultiBinding is not supported
To compensate for the above, I tried consolidating the converter logic into a single string property of the Row but the DataTemplate doesn't appear to be assigned. Also the binding syntax I used gives runtime XAML errors, not sure why.
<ItemsControl
ItemsSource="{Binding Items}" >
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="ContentTemplate">
<Setter.Value>
<Binding Path="RowTemplate" Converter="{StaticResource RowTemplateConverter}"/>
</Setter.Value>
</Setter>
</Style>
</ItemsControl.ItemContainerStyle>
DataTemplateSelector and ItemContainerStyleSelectors won't work because they're only evaluated once, and I need the templates updated on various property changes
I've seen some answers here that say to null the above Selectors and re-assign them. This is the closest I've been able to get to my desired behavior, but the performance is poor with dozens of items, and fast changing properties, so I'm unable to use this.
You can write an attached behavior to accomplish this. Alternatively extend e.g. ItemsControl (or a derived type).
The key is to reassign the item container's content in order to invoke the DataTemplateSelector again.
The attacehed property will reset the content to trigger the DataTemplateSelector. Your view model will track the changes of the data items that require to re-evaluate the actual DataTemplate and finally trigger the attached property. This is done by simply assigning the changed item to a view model property that binds to the attached behavior.
First create a template selector by extending DataTemplateSelector:
public class DataItemTemplateSelector : DataTemplateSelector
{
public DataTemplate ActivatedTemplate { get; set; }
public DataTemplate DeactivatedTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
switch (item)
{
case DataItem dataItem when dataItem.IsActivated: return this.ActivatedTemplate;
default: return this.DeactivatedTemplate;
}
}
}
Implement the attached behavior that modifies the container of the changed item:
public class TemplateSelector : DependencyObject
{
public static object GetChangedItem(DependencyObject obj)
{
return (object)obj.GetValue(ChangedItemProperty);
}
public static void SetChangedItem(DependencyObject obj, object value)
{
obj.SetValue(ChangedItemProperty, value);
}
public static readonly DependencyProperty ChangedItemProperty =
DependencyProperty.RegisterAttached("ChangedItem", typeof(object), typeof(TemplateSelector), new PropertyMetadata(default(object), OnChangedItemChanged));
private static void OnChangedItemChanged(DependencyObject attachingElement, DependencyPropertyChangedEventArgs e)
{
if (!(attachingElement is ItemsControl itemsControl))
{
throw new ArgumentException($"Attaching element must be of type '{nameof(ItemsControl)}'");
}
var container = (itemsControl.ItemContainerGenerator.ContainerFromItem(e.NewValue) as ContentControl);
var containerContent = container.Content;
container.Content = null;
container.Content = containerContent; // Trigger the DataTemplateSelector
}
}
Apply the attached property and bind it to your view model. Also assign the template selector:
<Page>
<Page.Resources>
<local:DataItemTemplateSelector x:Key="TemplateSelector">
<local:DataItemTemplateSelector.ActivatedTemplate>
<DataTemplate x:DataType="local:DataItem">
<TextBlock Text="{Binding Text}" Foreground="Red" />
</DataTemplate>
</local:DataItemTemplateSelector.ActivatedTemplate>
<local:DataItemTemplateSelector.DeactivatedTemplate>
<DataTemplate x:DataType="local:DataItem">
<TextBlock Text="{Binding Text}" Foreground="Black" />
</DataTemplate>
</local:DataItemTemplateSelector.DeactivatedTemplate>
</local:DataItemTemplateSelector>
</Page.Resources>
<Grid>
<ListBox ItemsSource="{x:Bind MainViewModel.DataItems}"
local:TemplateSelector.ChangedItem="{x:Bind MainViewModel.UpdatedItem, Mode=OneWay}"
ItemTemplateSelector="{StaticResource TemplateSelector}" />
</Grid>
</Page>
Finally let the view model track the relevant property changes and set the changed property e.g. to a UpdatedItem property which binds to the attached behavior (see above):
public class MainViewModel : ViewModel, INotifyPropertyChanged
{
public MainViewModel()
{
DataItems = new ObservableCollection<DataItem>();
for (int index = 0; index < 10; index++)
{
DataItem newItem = new DataItem();
// Listen to property changes that are relevant
// for the selection of the DataTemplate
newItem.Activated += OnItemActivated;
this.DataItems.Add(newItem);
}
}
// Trigger the attached property by setting the property that binds to the behavior
private void OnItemActivated(object sender, EventArgs e) => this.UpdatedItem = sender as DataItem
public ObservableCollection<DataItem> DataItems { get; }
private DataItem updatedItem;
public DataItem UpdatedItem
{
get => this.updatedItem;
set
{
this.updatedItem = value;
OnPropertyChanged();
}
}
}
this only updates the container of the item that you select in the view model.
Yep, The DataItemTemplateSelector works when preparing items. it will not response the item's property change even if it has implement INotifyPropertyChanged interface, the better way is use IValueConverter to update the uielement base on the specific property.
For example
public class ImageConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
object img = null;
switch (value.ToString())
{
case "horizontal":
img = new BitmapImage(new Uri("ms-appx:///Assets/holder1.png"));
break;
case "vertical":
img = new BitmapImage(new Uri("ms-appx:///Assets/holder2.png"));
break;
default:
break;
}
return img;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
For please refer IValueConverter document.

WPF DataGrid display 'NULL' when bound property is null

I'm trying to display a cell containing a 'NULL' string instead of a blank cell when the corresponding source value is null. I'm using a DataGrid bound to a DataTable and AutoGenerateColumns="True".
Previously I managed to do it in the code-behind through the AutoGeneratedColumns event, but now I've switched to MVVM design and I wanted to avoid this.
foreach (var column in dgwCustomTableSelected.Columns)
{
if (column is DataGridTextColumn)
{
((DataGridTextColumn)column).Binding =
new Binding() {
Converter = new NullValueConverter((string)column.Header)
};
}
}
I was wondering whether there's a way to associate a Converter to all the datagrid columns or any other feasible solution to this.
Thanks in advance
I was wondering whether there's a way to associate a Converter to all the datagrid columns or any other feasible solution to this.
The "feasible solution" would be to handle the AutoGeneratingColumn event in the view. Or define all columns explicitly in the XAML markup of the same view.
Neither approach breaks the MVVM pattern since this is view-related functionality and MVVM is not about eliminating view-related code from the views. It is mainly about separation of concerns and doing view-related things programmatically instead of doing it in the XAML markup is perfectly fine.
I would try to do it in the ViewModel. Lets say your ViewModel class would look something like this (I left INotofyPropertyChanged out for simplicity):
public class ViewModel
{
private ModelClass model = new ModelClass();
public string Name
{
get
{
return model.Name;
}
set
{
model.name = value;
}
}
}
You could refactor it to something like this:
public class ViewModel
{
private ModelClass model = new ModelClass();
public string Name
{
get
{
if(model.Name == null)
{
return "NULL";
}
return model.Name;
}
set
{
model.name = value;
}
}
}
You could also try Custom Markup Extension that will allow you to include Converter inside of the Binding. For that to work you would also have to create DataTemplates for different types of data but overall gain, namely handling the data types, would outweigh the amount of coding. Here is an example of said Markup Extension from searchwindevelopment
class NumberToBrushConverter : MarkupExtension, IValueConverter
{
private static NumberToBrushConverter _converter = null;
public override object ProvideValue(IServiceProvider serviceProvider)
{
// determine if we have an instance of converter
// return converter to client
return _converter ?? (_converter = new NumberToBrushConverter());
}
public object Convert(object value, Type targetType,
object parameter,CultureInfo culture)
{
return new SolidColorBrush(Colors.Orange);
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Then in xaml you would use it like this:
<Window x:Class="ValueConverterTips.CustomMarkupTip"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters='clr-namespace:ValueConverterTips.Converters'
Title="CustomMarkupTip" >
<!-- no longer need the resources section -->
<Grid>
<Ellipse Fill='{Binding SomeIntData,
Converter={converters:NumberToBrushConverter}}'
Width='10' Height='10' />
</Grid>
</Window>
You would have to modify this to suit your scenario.
EDIT
This is how you would use this with AutoGenerateColumns=true:
<Style TargetType="DataGridColumnHeader">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Button Content="Ok"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<DataGrid AutGenerateColumns="true" ItemsSource={Binding xxx} etc...>
</DataGrid>

Control Content and Property change

I have dialogbox with Content control with templates:
<ContentControl Content="{Binding Model,UpdateSourceTrigger=PropertyChanged}" ContentTemplateSelector="{StaticResource TemplateSelector}"/>
and property change event at dialogbox context:
dialogContext.Model.PropertyChanged += (s, e) => Change(s,e, context);
private void Change(object s, PrropertyChangeEventArgs e, Context context)
{
...
context.Mode = new Example()
{
...
}
model.PropertyChanged += (sender, eventArgs) =>
ModelChange(sender, eventArgs, context);
context.Model = model;
}
I want to change some properties at model, that determine which custom template will be displayed.
To reload new template and invoke temlate selector should I create new model and
add property change event to this. Is is ok, or is it another way to do this.
Update
The below implementation doesn't work because it turns out that the template selector is only reinvoked if the actual value of ContentControl.Content changes. If you've still got the same instance of Model, raising PropertyChanged will have no effect. I even tried overriding ModelClass.Equals() and ModelClass.GetHashCode(). Neither was called. Maybe the Binding is calling Object.ReferenceEquals().
But I did find three ways to do this. All have been tested, now that I've learned my lesson.
If you're going to this much trouble to get a template selector to work, best to look for some other approach where you're not fighting the framework.
You could instead use style triggers to swap templates:
<ContentControl
Content="{Binding Model}"
>
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding Model.Foo}" Value="foo">
<Setter
Property="ContentTemplate"
Value="{StaticResource Foo}"
/>
</DataTrigger>
<DataTrigger Binding="{Binding Model.Foo}" Value="bar">
<Setter
Property="ContentTemplate"
Value="{StaticResource Bar}"
/>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
...but the logic in your template selector may be quite a bit more complicated than that, in which case it may not be feasible.
Here's another. You don't need a template selector to select a template. A converter can return a DataTemplate too, and if you use a multi-binding converter, you can give it whatever it needs to look up a DataTemplate in the resources:
<ContentControl
Content="{Binding Model}"
>
<ContentControl.ContentTemplate>
<MultiBinding
Converter="{StaticResource ContentTemplateConverter}"
>
<!--
We must bind to Model.Foo so the binding updates when that changes,
but we could also bind to Model as well if the converter wants to
look at other properties besides Foo.
-->
<Binding Path="Model.Foo" />
<!-- The ContentControl itself will be used for FindResource() -->
<Binding RelativeSource="{RelativeSource Self}" />
</MultiBinding>
</ContentControl.ContentTemplate>
</ContentControl>
C#
public class ContentTemplateConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var ctl = values[1] as FrameworkElement;
switch ($"{values[0]}")
{
case "foo":
return ctl.FindResource("Foo") as DataTemplate;
case "bar":
return ctl.FindResource("Bar") as DataTemplate;
}
return null;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
One last possibility, and in my opinion the least, is to use the template selector, but make it work by actually replacing the value of Model every time one of its properties changes. Rewrite ModelClass so it can easily be cloned:
public ModelClass() {}
public ModelClass(ModelClass cloneMe) {
this.Foo = cloneMe.Foo;
this.Bar = cloneMe.Bar;
}
...and keep _model_PropertyChanged from my original answer, but change the guts so instead of merely raising PropertyChanged, it replaces the actual value of Model (which will of course still raise PropertyChanged, as a side effect):
private void _model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ModelClass.Foo))
{
Model = new ModelClass(Model);
}
}
I've tested that and while it's alarmingly goofy, it does work.
Instead of cloning ModelClass, you could use a "reference" class for the parent's Model property:
public class ModelClassRef {
public ModelClassRef(ModelClass mc) { ... }
public ModelClassRef { get; private set; }
}
But it's still wicked goofy. The viewmodel shouldn't "know" the view even exists, but here you are rewriting a chunk of it in a bizarre way just to work around a peculiarity in the implementation of a particular control. View workarounds belong in the view.
So when this.Model.Foo changes, you want to change the template? I would expect this to do the job:
#region Model Property
private ModelClass _model = null;
public ModelClass Model
{
get { return _model; }
set
{
if (value != _model)
{
if (_model != null)
{
_model.PropertyChanged -= _model_PropertyChanged;
}
_model = value;
if (_model != null)
{
_model.PropertyChanged += _model_PropertyChanged;
}
OnPropertyChanged();
}
}
}
private void _model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// If Model.Foo changed, announce that Model changed. Any binding using
// the Model property as its source will update, and that will cause
// the template selector to be re-invoked.
if (e.PropertyName == nameof(ModelClass.Foo))
{
OnPropertyChanged(nameof(Model));
}
}
This is defined in your viewmodel base class. Maybe you've already got essentially the same method and it's called something else; if so, use that one of course.
protected void OnPropertyChanged([CallerMemberName] String propName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
By the way, get rid of UpdateSourceTrigger=PropertyChanged. ContentControl will never create a new value for its Content property and pass that back to your viewmodel through the binding. Can't, won't, and you wouldn't want it to. So you don't need to tell it exactly when to perform a task it's not capable of performing.

How do I disable a button bound to a current item's ICommand when there is no current item?

Say you have a button whose command property is bound to some ICommand of the current item of some collection.
When the collection is null, the button remains enabled and clicking it seems to be a no-op. I want instead that the button remains disabled. I figured out the following to keep buttons disabled whenever the collection is null. It however seems a bit too convoluted for something that could perhaps be accomplished in a more natural, simpler, and more MVVM like.
Hence the question: is there a simpler way to keep that button disabled, ideally where no code-behind is used?
.xaml:
<Button Content="Do something" >
<Button.Command>
<PriorityBinding>
<Binding Path="Items/DoSomethingCmd" />
<Binding Path="DisabledCmd" />
</PriorityBinding>
</Button.Command>
</Button>
.cs:
public class ViewModel : NotificationObject
{
ObservableCollection<Foo> _items;
public DelegateCommand DisabledCmd { get; private set; }
public ObservableCollection<Foo> Items {
get { return _items; }
set { _items = value; RaisePropertyChanged("Items"); }
}
public ViewModel()
{
DisabledCmd = new DelegateCommand(DoNothing, CantDoAnything);
}
void DoNothing() { }
bool CantDoAnything()
{
return false;
}
}
Edit:
A couple of notes:
I am aware that I can use lambda expressions, but in this example code I do not.
I know what a predicate is.
I don't see how doing something with DoSomethingCmd.CanExecute will do anything to help as there is no DoSomethingCmd to access while there is no current item.
So I'll re-center my question: How can I avoid using the DisabledCmd? I am not interested in moving up the DoSomethingCmd as it is not what I am looking for. I wouldn't be asking this question otherwise.
Another edit:
So I basically adopted this answer as a solution: WPF/MVVM: Disable a Button's state when the ViewModel behind the UserControl is not yet Initialized?
It is, I believe, exactly what hbarck proposes.
I'd do it similar to akjoshi, only I'd use a normal Trigger instead of a DataTrigger, and I'd check on Button.Command to be null. Since it always makes sense to disable a Button that has no Command (especially in MVVM, where there are no click eventhandlers), it would also be a good idea to include this trigger into a default style for Buttons, in order to have this behaviour on all Buttons across the application... I don't see a reason to use a dummy command.
You can create a Trigger to check if the Item (data context of button) is null, and set Button's (or may be it's parent container's as Anton mentioned) IsEnabled property to false, something like this -
<DataTrigger
Binding="{Binding Path=Item}"
Value="{x:Null}">
<Setter Property="Control.IsEnabled" Value="False" />
</DataTrigger>
I am not in position to test it right now, but I think this should work.
Looking at the code in PresentationFramework.dll, I don't see any straightforward way to do it (see ButtonBase.UpdateCanExecute). You might have some luck deriving a class from Button and overriding the metadata for CommandProperty to handle changes yourself. But you can easily avoid having that do-nothing-command code in your viewmodel: create a special converter which will convert a null command to a shared always-disabled fallback ICommand. If you have lots of buttons that need this kind of behavior, an attached property and a style may be in order.
If you look at delegate command, the second parameter is a func that enables you to do exactly this, I am not quite sure, why are you making it so complex.
If you do for example:
DoSomethingCommand = new DelegateCommand(() => SampleAction, () => Items != null);
the button will be disabled when you simply bind its Command property to this command, like so:
<Button Command={Binding DoSomethingCommand} />
The button will then be automatically disabled when the condition in the delegate command becomes false. You also should call DoSomethingCommand.RaiseCanExecuteChanged() when the condition's outcome could change, than the button's IsEnabled updates to reflect the current state.
I used RelayCommands and this has a constructor where you can create a canExecute Predicate and then if it returns false the bound button will be disabled automatically.
On the delegate command you should rewrite the CantDoAnything() method to represent your enable and disable logic. And the binding you should simply bind to the Command.
DelegateCommand constructor on MSDN
DelegateCommand CanExecute BugFix
You can simply bind the button's IsEnabled property to the current item and use a converter.
Here's a complete demo:
<Page.Resources>
<Converters:NullToBoolConverter x:Key="NullToBoolConverter"
IsNullValue="False" IsNotNullValue="True" />
</Page.Resources>
<Page.DataContext>
<Samples:NoCurrentItemViewModel/>
</Page.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListBox
IsSynchronizedWithCurrentItem="True" Grid.Row="0"
ItemsSource="{Binding Items}"
DisplayMemberPath="Name"/>
<Button
Grid.Row="1"
Content="Do something"
IsEnabled="{Binding Items/, Converter={StaticResource NullToBoolConverter}}"
Command="{Binding Items/DoSomethingCommand}"/>
<Button Grid.Row="2" Content="Clear" Command="{Binding ClearCommand}"/>
</Grid>
View models - RelayCommand from MVVM Light
public class NoCurrentItemViewModel
{
public NoCurrentItemViewModel()
{
_items = new ObservableCollection<NoCurrentItemDetail>
{
new NoCurrentItemDetail{Name = "one"},
new NoCurrentItemDetail{Name = "two"},
};
ClearCommand = new RelayCommand(Clear);
}
public ICommand ClearCommand { get; private set; }
private void Clear()
{
_items.Clear();
}
private readonly ObservableCollection<NoCurrentItemDetail> _items;
public IEnumerable<NoCurrentItemDetail> Items
{
get { return _items; }
}
}
public class NoCurrentItemDetail
{
public NoCurrentItemDetail()
{
DoSomethingCommand = new RelayCommand(DoSomething);
}
private void DoSomething()
{
Debug.WriteLine("Do something: " + Name);
}
public ICommand DoSomethingCommand { get; private set; }
public string Name { get; set; }
}
The converter
public class NullToBoolConverter : IValueConverter
{
public NullToBoolConverter()
{
IsNullValue = true;
IsNotNullValue = false;
}
public bool IsNullValue { get; set; }
public bool IsNotNullValue { get; set; }
#region Implementation of IValueConverter
public object Convert(object value,
Type targetType, object parameter, CultureInfo culture)
{
return value == null ? IsNullValue : IsNotNullValue;
}
public object ConvertBack(object value,
Type targetType, object parameter, CultureInfo culture)
{
return Binding.DoNothing;
}
#endregion
}

How to disable a button if no items are selected in a ListView

I have a ListView Contained in a UserControl I would like to disabled a button when no items are selected in the UserControl, would it be the right way to do it? So far, it doesn't disable, it just stays enable all the way.
I've included the xaml code.
searchAccountUserControl is the UserControl name property in the xaml.
And AccountListView is the ListView name property in the userControl xaml.
<Button Content="Debit" IsEnabled="true" HorizontalAlignment="Left" Margin="18,175,0,0" Name="DebitButton" Width="128" Grid.Column="1" Height="32" VerticalAlignment="Top" Click="DebitButton_Click">
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=searchAccountUserControl.AccountListView, Path=SelectedValue}" Value="{x:Null}" >
<Setter Property="Button.IsEnabled" Value="false"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
Thanks.
Finally i've used :
in my ViewModel :
private bool _isSelected;
public bool IsSelected { get { return _isSelected; }
set { _isSelected = _account.View.CurrentItem != null;
PropertyChanged.SetPropertyAndRaiseEvent(this, ref _isSelected, value,
ReflectionUtility.GetPropertyName(() => IsSelected)); } }
And then Use isEnabled = "{Binding Path=IsSelected}" in the xaml.
There are a few things wrong here.
Precedence, if you set IsEnabled on the control itself the style will never be able to change it.
ElementName, it's an ElementName, not a path, just one string that gives the name of one element. Everything beyond that goes into the Path.
Style syntax, if you set a Style.TargetType you should not set the Setter.Property with a type prefix (although leaving it does not break the setter).
By the way, this alone is enough:
<Button IsEnabled="{Binding SelectedItems.Count, ElementName=lv}" ...
It's obvious that you aren't using Commanding (ICommand Interface). You should either use that (and preferably the Model-View-ViewModel architecture).
But, if you want to stick with code-behind and XAML:
<ListView SelectionChanged="AccountListView_SelectionChanged" ... />
private void AccountListView_SelectionChanged(Object sender, SelectionChangedEventArgs args)
{
DebitButton.IsEnabled = (sender != null);
//etc ...
}
More information on MVVM: http://msdn.microsoft.com/en-us/magazine/dd419663.aspx
You need to set the DataContext of the View (UserControl) to the instance of the ViewModel you want to use. Then, from there, you can bind to properties on the ViewModel, including ICommands. You can either use RelayCommand (see link above) or use Commanding provided by a framework (for example, Prism provides a DelegateCommand). These commands take an Action (Execute) and a Func (CanExecute). Simply provide the logic in your CanExecute. Of course, you'd also have to have your ListView SelectedItem (or SelectedValue) be databound to a property on your ViewModel so you can check to see if it's null within your CanExecute function.
Assuming you use RelayCommand you don't have to explicitly call the RaiseCanExecuteChanged of an ICommand.
public class MyViewModel : ViewModelBase //Implements INotifyPropertyChanged
{
public MyViewModel()
{
DoSomethingCommand = new RelayCommand(DoSomething, CanDoSomething);
}
public ObservableCollection<Object> MyItems { get; set; }
public Object SelectedItem { get; set; }
public RelayCommand DoSomethingCommand { get; set; }
public void DoSomething() { }
public Boolean CanDoSomething() { return (SelectedItem != null); }
}
<ListView ItemsSource="{Binding MyItems}" SelectedItem="{Binding SelectedItem}" ... />
<Button Command="{Binding DoSomethingCommand}" ... />

Categories

Resources