I have some XAML that looks (trimmed) like this, with a button tying its IsEnabled attribute to a subproperty:
<Grid DataContext="{Binding RelativeSource={RelativeSource AncestorType=Window}}">
...
<Button x:Name="continueButton" Content="Continue" IsEnabled="{Binding CurrentQuestion.AnswerSelected, Mode=OneWay}" Click="continueButton_Click"/>
...
CurrentQuestion is a property that pulls the current one from a collection:
public Question CurrentQuestion {
get
{
return Questions[QuestionNo];
}
set
{
Questions[QuestionNo] = value;
}
}
AnswerSelected checks whether any of the Answers are marked as selected.
public bool AnswerSelected
{
get
{
return Answers.Any(a => a.Selected);
}
}
The Selected property itself is set by radio buttons bound to the possible answers. Thus, the user should be able to continue after choosing an answer.
The Answers collection is monitored for changes, and calls the OnPropertyChanged() method from INotifyPropertyChanged for the AnswerSelected bool property like so:
public Answer[] Answers
{
get
{
return _answers;
}
set
{
_answers = value;
OnPropertyChanged("Answers");
OnPropertyChanged("AnswerSelected");
}
}
The binding successfully sets the button to disabled to begin with, but no change to the radio buttons then re-enables the button. I tried moving AnswerSelected to be a property at the same level as CurrentQuestion but this didn't work either.
What am I missing to get this button to re-enable? Also, is there a better way to accomplish this same thing?
Edit:
This is the code for the radio button setting.
<Grid DataContext="{Binding CurrentQuestion, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}">
<ItemsControl ItemsSource="{Binding Answers}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<RadioButton GroupName="AnswerGroup" IsChecked="{Binding Selected}">
<TextBlock Text="{Binding Text}" />
</RadioButton>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
So it looks like this:
CurrentAnswer (Questions[QuestionNo])
AnswerSelected (Answers.Any(a => a.Selected))
Edit 2:
I think my question, effectively, is this: The bound property is a calculated property, but the calculation uses the subproperty of an array element. How do I, therefore, raise notification when that subproperty is changed, which itself is in a different class that defines each array element?
What I eventually needed to do, in a way that was influenced by this question, was to subscribe to the PropertyChanged event of each Answer from the Question class.
void SubscribeToSelect(Answer item)
{
item.PropertyChanged += (s, e) => this.AnswerSelected = e.PropertyName == "Selected" && item.Selected ? true : this.AnswerSelected;
}
When that fired, I updated the Question.AnswerSelected property and fired an update on that too, which in turn updated the binding to the button:
private bool _answerSelected;
public bool AnswerSelected
{
get
{
return _answerSelected;
}
set
{
_answerSelected = value;
OnPropertyChanged("AnswerSelected");
}
}
Related
Perhaps I'm going about this the wrong way, but my layout is in a way where I have multiple Expanders in a TabControl and I want to add an "expand all" button.
Now logically this button should be inside the tab as it would control the elements in the tab so they ought to be grouped together. Visually however this would be a waste of space as I got a lot of empty space on the Tab Header bar (not sure what the terminology is, the row with the tabheaders).
So what I'm trying to achieve is adding a button outside the content of the tab. The canvas element seems to be what I need to use and it's working as far as its repositioning the element but it gets cut off. This is much easier to explain with a picture so
(if you look hard you can see where the button is as the header covering it is slightly translucent)
Now I can position it where I'd like it to be by moving it outside the TabItem but then I would have to write code to see which tab is focussed and hide it when it's not "Current" that is focussed. That to me sounds like the wrong way to do it as the only thing I want to do is move a button which is a 'view' type of thing.
My MainWindow.axaml:
<TabControl Grid.Row="1" Grid.Column="0" VerticalAlignment="Stretch">
<TabItem Header="Current" ZIndex="1">
<ScrollViewer Classes="CrawlersInAction">
<StackPanel>
<Canvas>
<Button Canvas.Right="10" Canvas.Top="-20" ZIndex="5">Expand All</Button>
</Canvas>
<!-- My very long template code for rendering the expanders -->
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
I do have a background in HTML/CSS so I thought Zindex would the trick and tried applying it in various places without any luck.
PS: I'm using Avalonia instead of WPF but it's pretty much a cross-platform clone, so any WPF know-how probably carries over 1:1.
If you think about it, this functionality lives in the ViewModel at the same "level" as the Tab Control.
<Grid>
<TabControl Items="{Binding MyTabViewModels}" SelectedItem={Binding SelectedTab} />
</Grid>
An Instance of MyTabViewModel has a collection on it:
public ObservableCollection<MyCollectionType> Items
The item class MyCollectionType has an IsExpanded property ...
public bool IsExpanded {get;set;}
Bound to your Expander control IsExpanded property.
Shove your button into the XAML
<Grid>
<TabControl Items="{Binding MyTabViewModels}" />
<Button Commmand={Binding ExpandAllCommand} />
</Grid>
Now on your base ViewModel your ICommand can do something like:
public void ExpandAllCommandExecuted()
{
foreach(var vm in SelectedTab.Items)
{
vm.IsExpanded = true;
}
}
Good luck, this is all pseudocode but illustrates a potential pattern.
The problem seems to have originated from placing my <canvas> control inside the <scrollviewer> control. I've placed it outside it whilst trying many things it seems and it works as I wanted it to. The buttons are visible rendering on top of the tabbar (TabStrip).
My XAML is now:
<TabControl Grid.Row="1" Grid.Column="0" VerticalAlignment="Stretch">
<TabItem Header="Current">
<StackPanel>
<Canvas>
<StackPanel Orientation="Horizontal" Canvas.Right="0" Canvas.Bottom="10" Spacing="5">
<Button Command="{Binding CollapseAll}" IsEnabled="{Binding !AllAreCollapsed}">Collapse All</Button>
<Button Command="{Binding ExpandAll}" IsEnabled="{Binding !AllAreExpanded}">Expand All</Button>
</StackPanel>
</Canvas>
<ScrollViewer Classes="CrawlersInAction">
<StackPanel>
<ItemsControl Name="itemscontrol" Items="{Binding SiteInfos}" VerticalAlignment="Stretch">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Expander ExpandDirection="Down" IsExpanded="{Binding IsExpanded, Mode=TwoWay}" VerticalAlignment="Stretch">
<!-- Ommited my very long template code -->
</Expander>
<DataTemplate>
<ItemsControl.ItemTemplate>
<ItemsControl>
</StackPanel>
</ScrollViewer>
</StackPanel>
</TabItem>
</TabControl>
Codewise I ended up adding a "IsExpanded" property to my SiteInfo class that is used as the base for the expanders IsExpanded property and kept in sync by making it a two way binding as per the XAML above. The code on SiteInfo is:
public class SiteInfo : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public static readonly bool StartIsExpanded = true;
private bool _isExpanded = StartIsExpanded;
public bool IsExpanded
{
get { return _isExpanded; }
set
{
if (value != IsExpanded)
{
_isExpanded = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsExpanded)));
}
}
}
When I create my SiteInfo objects in MainWindowViewModel I subscribe to the events (siteInfo.PropertyChanged += SiteInfo_PropertyChanged;). When the event is received and it would change if my collapse or expand all button should be disabled it sends it own PropertyChangedEvent which then enables/disabled the control.
public class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged
{
public new event PropertyChangedEventHandler? PropertyChanged;
public ObservableCollection<SiteInfo> SiteInfos { get; }
= new ObservableCollection<SiteInfo>();
//Change SiteInfo.StartExpanded if you want this changed.
private bool _allAreExpanded = SiteInfo.StartIsExpanded;
public bool AllAreExpanded
{
get => _allAreExpanded;
set
{
if (_allAreExpanded != value)
{
_allAreExpanded = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AllAreExpanded)));
}
}
}
//Change SiteInfo.StartExpanded if you want this changed.
private bool _allAreCollapsed = !SiteInfo.StartIsExpanded;
public bool AllAreCollapsed {
get { return _allAreCollapsed; }
set {
if (_allAreCollapsed != value)
{
_allAreCollapsed = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AllAreCollapsed)));
}
}
}
private void SiteInfo_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if(e.PropertyName == nameof(siteInfo.IsExpanded))
{
AllAreCollapsed = AreAllCollapsed();
AllAreExpanded = AreAllExpanded();
}
}
public bool AreAllCollapsed()
{
return !SiteInfos.Any<SiteInfo>( siteInfo => siteInfo.IsExpanded );
}
public bool AreAllExpanded()
{
return !SiteInfos.Any<SiteInfo>( siteInfo => siteInfo.IsCollapsed);
}
public void CollapseAll()
{
foreach(SiteInfo siteInfo in SiteInfos)
{
siteInfo.IsExpanded = false;
}
}
public void ExpandAll()
{
foreach (SiteInfo siteInfo in SiteInfos)
{
siteInfo.IsExpanded = true;
}
}
}
Figured I'd add the rest of my code in case anyone Googles this up and wants to do something similar.
So now when my program loads and everything is set to the default expanded true Expand All is disabled, Collapse all is enabled. Changing one expander to collapsed status will have both buttons enabled and collapsing all expanders will disable the Collapse All button.
I am new to WPF and have come across a problem.
I have an MVVM WPF application and I want to implement filtering to my DataGrid. I have tried all possible solutions on the internet, but none of them work for me for some reason. I have created a TextBox and binded it to FilterName. What I want it to do is on every keypress, the value of FilterName should be updated and the filter should be triggered. Unfortunately, the filter triggers only once - when I start the application and by putting a breakpoint in the Set block of FilterName, I have discovered that it never reaches it.
Here is the declaration of the TextBox:
<TextBox
x:Name="FilterName"
MinWidth="150"
Margin="{StaticResource SmallTopBottomMargin}"
Background="Transparent"
BorderThickness="0,0,0,1"
Text="{Binding FilterName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, IsAsync=True}"
TextWrapping="Wrap" />
And here is the ViewModel:
private readonly ObservableCollection<PersonData> _data;
public ICollectionView DataCollectionView { get; }
private string _filterName = string.Empty;
public string FilterName
{
get
{
return _filterName;
}
set
{
_filterName = value;
DataCollectionView.Refresh();
}
}
public MainWindowViewModel(ISampleDataService sampleDataService)
{
//Adding the data here
DataCollectionView = CollectionViewSource.GetDefaultView(_data);
DataCollectionView.Filter = FilterByName;
}
private bool FilterByName(object obj)
{
if (obj is PersonData data)
{
return data.Name.Contains(FilterName, StringComparison.InvariantCultureIgnoreCase);
}
return false;
}
This binding should work provided that the view model with the FilterName property is the DataContext of the parent window:
Text="{Binding DataContext.FilterName, UpdateSourceTrigger=PropertyChanged,
RelativeSource={RelativeSource AncestorType=Window}}"
Set name of your window to x:Name="_this" and change the TextBox binding:
<TextBox
x:Name="tbFilterName"
DataContext="{Binding ElementName=_this}"
Text="{Binding Path=FilterName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
TextChanged="FilterName_TextChanged"
...
Remove DataCollectionView.Refresh(); call from the FilterName setter, but add
private void FilterName_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
DataCollectionView.Refresh();
}
I have a ListView bound to a collection of objects (called Users, in this case), and the template includes a ContextActions menu. One of the menu items needs to be enabled or disabled depending on a condition having nothing directly to do with the items in the view (whether or not there's a Bluetooth connection to a certain kind of peripheral). What I'm doing right now is iterating the Cells in the TemplatedItems property and setting IsEnabled on each.
Here's the XAML for the ListView, stripped down to the parts that matter for my question:
<ListView ItemsSource="{Binding .}" ItemTapped="item_Tap">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Label}">
<TextCell.ContextActions>
<MenuItem
Text="Copy to other device"
ClassId="copyMenuItem"
Clicked="copyMenuItem_Click" />
</TextCell.ContextActions>
</TextCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Here's how I'm setting the property values now:
foreach (Cell cell in usersListView.TemplatedItems)
{
foreach (MenuItem item in cell.ContextActions)
{
if ("copyMenuItem" == item.ClassId)
{
item.IsEnabled = isBluetoothConnected;
}
}
}
That works, but i don't like it. It's obviously out of line with the whole idea of data-bound views. I'd much rather have a boolean value that I can bind to the IsEnabled property, but it doesn't make sense from an object design point of view to add that to the User object; it has nothing to do with what that class is about (representing login accounts). I thought of wrapping User in some local class that exists just to tape this boolean property onto it, but that feels strange also since the value will always be the same for every item in the collection. Is there some other way to bind the MenuItem.IsEnabled property?
Use relative binding
Get ready in your view model class, inherit INotifyPropertyChanged or your BaseViewModel.
public class YourViewModel : INotifyPropertyChanged
{
private string isBluetoothConnected;
public string IsBluetoothConnected
{
get => isBluetoothConnected;
set => SetProperty(ref isBluetoothConnected, value);
}
public ObservableCollection<User> Users { get; private set; }
}
Add a name to ListView for reference, and apply relative binding in MenuItem.
<ListView
x:Name="UserListView"
ItemsSource="{Binding Users}"
ItemTapped="item_Tap">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Label}">
<TextCell.ContextActions>
<MenuItem
IsEnabled="{Binding Path=BindingContext.IsBluetoothConnected, Source={x:Reference UserListView}}"
Text="Copy to other device"
ClassId="copyMenuItem"
Clicked="copyMenuItem_Click" />
</TextCell.ContextActions>
</TextCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
It turns out that this case of BindableProperty is, in fact, not bindable: https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/menuitem#enable-or-disable-a-menuitem-at-runtime
One must add a Command property to the MenuItem and assign a BindingContext to that, and set its executability. Here's the latest version of my code, which does work:
<MenuItem
Text="Copy to other device"
Clicked="copyMenuItem_Click"
BindingContext="{x:Reference usersListView}"
Command="{Binding BindingContext.CopyCommand}" />
public class UsersViewModel
{
public Command CopyCommand { get; set; }
public bool IsBluetoothConnected
{
get { return isBluetoothConnected; }
set
{
isBluetoothConnected = value;
if (CopyCommand.CanExecute(null) != value)
{
CopyCommand.ChangeCanExecute();
}
}
}
public ObservableCollection<User> Users { get; private set; }
private bool isBluetoothConnected;
public async System.Threading.Tasks.Task<int> Populate( )
{
CopyCommand = new Command(( ) => { return; }, ( ) => IsBluetoothConnected); // execute parameter is a no-op since I really just want the canExecute parameter
IList<User> users = await App.DB.GetUsersAsync();
Users = new ObservableCollection<User>(users.OrderBy(user => user.Username));
return Users.Count;
}
}
I'm still not entirely happy with this; it contaminates the view model with the concerns of a specific view. I'm going to see if I can separate the Command from the view model. But it does accomplish my primary goal, bringing this UI implementation into the data binding paradigm.
I have 5 CheckBox and this is what they look like in the View:
<CheckBox IsChecked="{Binding Path=IsSelected, Mode=TwoWay}" Content="Cb1"/>
<CheckBox IsChecked="{Binding Path=IsSelected, Mode=TwoWay}" Content="Cb2"/>
<CheckBox IsChecked="{Binding Path=IsSelected, Mode=TwoWay}" Content="Cb3"/>
<CheckBox IsChecked="{Binding Path=IsSelected, Mode=TwoWay}" Content="Cb4"/>
<CheckBox IsChecked="{Binding Path=IsSelected, Mode=TwoWay}" Content="Cb5"/>
This is the some of the code that I have in my ViewModel:
class CheckBoxesViewModel : INotifyPropertyChanged
{
public CheckBoxesViewModel()
{
CheckBoxes= new ObservableCollection<Models.CheckBoxes>();
_canExecute = true;
}
private bool _IsSelected;
public bool IsSelected
{
get
{
return _IsSelected;
}
set
{
_IsSelected = value;
OnPropertyChanged("IsSelected");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
private ObservableCollection<Models.CheckBoxes> _checkBoxes = new ObservableCollection<Models.CheckBoxes>();
public ObservableCollection<Models.CheckBoxes> CheckBoxes
{
get { return _checkBoxes ; }
set
{
_checkBoxes = value;
OnPropertyChanged("CheckBoxes");
}
}
}
The problem is that when I check/uncheck one of the checkboxes it affects all of them.
I assume that is because they have exact same binding, but I can not figure out how to make the code distinguish them.
I think I could use Command and CommandParameters, but that does not seem like the best solution.
P.S. Do let me know if you see something wrong with my code - I am still trying to learn the whole MVVM thing.
You need to implement ICommand (google DelegateCommand to be able to treat an Action as ICommand) then you would bind the Command property of the CheckBox in the view to the Command on the view model.
ViewModel
public ICommand MyCommand { get; private set; }
.... MyCommand = new DelegateCommand((value) => this.DoStuff(value));
Xaml
<CheckBox Command={Binding MyCommand} Command Parameter={...} />
From your stated purpose in comments (which really should have been in your question--this is a classic XY problem)
You're attempting to route View logic through your ViewModel, which should be a hint that something's wrong here. Your stated purpose is
Each CheckBox has a corresponding TextBox that gets shown when it is checked. I was hoping to reuse the same code for all of the CheckBoxes and only change some value that helps me distinguish them (e.g. Content)
Toggling visibility is a View concern. You can do it thusly
<StackPanel>
<CheckBox x:Name = "cb1" />
<!-- cb2 through cbn omitted -->
<StackPanel />
<StackPanel>
<StackPanel.Resources>
<BooleanToVisibilityConverter x:Key="btvc" />
</StackPanel.Resources>
<TextBox Text="{Binding FirstTextBox}"
Visibility="{Binding IsChecked,
ElementName=cb1,
Converter={StaticResource btvc}}" />
<!-- SecondTextBox through NthTextBox omitted -->
</StackPanel />
I'm toggling visibility of the textbox by whether or not the corresponding checkbox was checked.
Now, if you're trying to munge together N textbox values into one property... You're making life too hard on yourself.
If you wish to 'programmatically add them", then you need to reverse your logic. Instead of 'adding controls' to the form, you need to think of 'adding data' to your ViewModel. This is how you stay within MVVM guidelines, as your tag suggests.
Here's how you can reverse the logic....The ItemsControl has the ability to bind to a collection of ViewModels. This control also 'automagically' determines what control to use for each item in the collection by using DataTemplates.
Here's the XAML code:
<ItemsControl ItemsSource="{Binding myCollection}" >
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type local:myViewModelForItemA}">
<CheckBox IsChecked="{Binding IsChecked}" Content="{Binding aName}"></CheckBox>
</DataTemplate>
<DataTemplate DataType="{x:Type local:myViewModelForItemB}">
<RadioButton IsChecked="{Binding IsChecked}" Content="{Binding aName}"></RadioButton>
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
As you can probably see, the Binding "myCollection" is the collection you add your ViewModel Instances to (this is an ObservableCollection).
Each DataTemplate in the IntemsControl.Resources is how you want each item to look (you can even place multiple controls inside each DataTemplate, just remember that whatever you bind these to will bind to the ViewModel in the collection (i.e. myViewModelForItemA, myViewModelForItemB).
The code in your main view model:
public ObservableCollection<object> myCollection { set; get; }
....
myCollection = new ObservableCollection<object>();
myViewModelForItemA anItem = new myViewModelForItemA();
myCollection.Add(anItem);
//now anItem of type (myViewModelForItemA) is in our collection
//and the ItemsControl automagically added a CheckBox to it's collection
//and bound isChecked to anItem.isChecked property, and bound
//the Content to anItem.aName property.
I submitted this accidently, I fixed it myself and wasn't going to submit after writing out the question. But have learnt from the comments, thanks!
I am trying to create a simple todo app in win8 and eventually want to hock it into ToDoIst API.
I have created a simple task class to try and get my head around the databinding however I just can not get it to do what I want to do. I have used listboxes and other basic form elements.
task.cs
class task
{
private string content;
private bool complete;
public string Content
{
get {return content;}
set { content = value; }
}
public bool Complete
{
get { return complete; }
set { complete = value; }
}
public task(string content)
{
Content = content;
Complete = false;
}
}
MainPage.xaml
And at the moment my XAML looks like this.
<GridView HorizontalAlignment="Left" Margin="482,190,0,0" VerticalAlignment="Top" Width="400" Height="500">
<ListView x:Name="LVtasks" HorizontalAlignment="Left" Height="500" VerticalAlignment="Top" Width="400" ItemsSource="{Binding}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Content}"/>
<RadioButton/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</GridView>
I have put in some dummy data, 4 elements and when I run it, it comes up with 4 boxes with radio buttons however no text (there is space for the text) I am not sure how I would bind the bool?
I can not see what I am doing wrong. If anyone could help and point me in the right direction, I have searched a fair amount of tutorials and just can not figure it out.
Your code looks a little strange, maybe this is what you want:
<ListView x:Name="LVtasks" HorizontalAlignment="Left" Height="500" VerticalAlignment="Top" Width="400" ItemsSource="{Binding ToDoItems}">
<ListView.ItemTemplate>
<DataTemplate>
<RadioButton GroupName="ToDos" Content="{Binding Content}" IsChecked="{Binding IsComplete}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Do you really want radiobuttons? I think you want Checkboxes, the difference is that when you use radiobuttons only one in a group can be 'checked'
I used this code behind to have a datacontext:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
ToDoItems = new ObservableCollection<TodoItem>(new List<TodoItem>
{
new TodoItem("Content1"),
new TodoItem("Content2")
});
this.DataContext = this;
}
public ObservableCollection<TodoItem> ToDoItems { get; set; }
}
I changed the name of task to ToDoItem Task is already a class in the framework and might cause confusion.
For the RadioButton IsChecked is the property to bind to a bool property:
<RadioButton IsChecked="{Binding Path=Complete}"/>
Your text is most likely not showing up because you haven't set up any change notifications and the binding is happening before you set the Content values. Using the INotifyPropertyChanged interface is the most common and usually the easiest way to do this.
Like John already mentioned you should really let the window containing your listview implement the INotifyPropertyChanged interface. And to set the data context of your window like Johan said. It is important that you call the propertychanged method in each setter of a property. It is also useful to use an ObservableCollection as ItemSource of your listview. Try to create an instance of ObservableCollection, create a property for it calling propertychanged method in its setter an set rhe ItemSource of your listview to the property. Do not forget to also call propertychanged whenever you add or remove items from the collection