Binding Visibility to Yes/No ComboBox with Converters - wpf

I've got a form that gets given a datarow from a dataset to bind all its elements. One of them is a bool, but I want that bool to be represented by by a Yes/No combo box. So I did this and it works nicely.
I also want to bind the visibility of a couple elements to this bool field. When the form loads, the initial setting of the visibility works. When I change the combobox selection, the ConvertBack() method of the ComboBox gets called (i.e. it's setting the bound value). But the other elements that have their visibility bound to that same field don't get updated. I set breakpoints in the Conversion methods and they never get called like they do when the form loads.
Here's the relevant XAML:
<ComboBox SelectedIndex="{Binding Path=[Adequate], Converter={StaticResource b2iConverter}}" Name="cb_Adequate" >
<ComboBoxItem>Yes</ComboBoxItem>
<ComboBoxItem>No</ComboBoxItem>
</ComboBox>
<Label Content="Reason:"
VerticalAlignment="Center"
Visibility="{Binding Path=[Adequate],
Converter={StaticResource b2vConverterInverse}}"/>
<TextBox Text="{Binding Path=[NotAdequateReason]}"
Visibility="{Binding Path=[Adequate],
Converter={StaticResource b2vConverterInverse}}"/>
"Adequate" is the bool field
b2iConverter is just booleanToIndexConverter (from the above link)
b2vConverterInverse is just an inverted boolean to visibility converter (I want the label and textbox shown when Adequate is FALSE or 0).
Thanks for any help. I can post more code if needed, I figure the problem is in the XAML...
EDIT: Apparently it's not possible with XAML (see Greg's post below), so I just do it in code:
private void cb_Adequate_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Visibility vis = (cb_Adequate.SelectedItem as ComboBoxItem).Content.ToString() == "Yes" ? Visibility.Collapsed : Visibility.Visible;
label_Reason.Visibility = tb_AdequateDesc.Visibility = vis;
}

If you want your UI elements to change state when a data property changes, you need to implement INotifyPropertyChanged on your data class.
This means that you can't use the DataRow for your purposes. You'll have to create a new class, then at run time populate it with values from the DataRow and then bind that object to your view.

Related

WPF Xaml GridView SelectionChanged Behavior

I am working with a WPF view with Prism.MVVM which allows our users to edit records.
Originally the record to be edited was selected via ComboBox.
<ComboBox IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Records}"
SelectedItem="{Binding SelectedRecord}"/>
This worked, but users wanted a more efficient way of finding which records had fields which needed updating so we have added a read only DataGrid which they can sort and visually spot which records they are interested in. Next they want to select the record to edit off the grid (but keep the combo box). This is where things go wrong.
Ideally the behavior we are looking for is:
If user selects a record from combo box:
The selected record is loaded in the form
The selected record is shown as selected in the combo box.
The selected record is shown as selected in the grid.
If user selects a record in Grid
single click to select record.
The selected record is loaded in the form
The selected record is shown as selected in the combo box
The selected record is shown as selected in the grid.
Most Successful Attempt
Trigger Command on SelectionChanged event of DataGrid
<DataGrid x:Name="TheDataGrid"
ItemsSource="{Binding Source={StaticResource GridRecords}}"
SelectedItem="DataContext.SelectedRecord, ElementName=LayoutRoot, Mode=OneWay}">
...
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction CommandParameter="{Binding SelectedItem, ElementName=TheDataGrid}"
Command="{Binding DataContext.SelectRecordFromGridCommand, ElementName=LayoutRoot}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
DelegateCommand:
Public ReadOnly Property SelectRecordFromGridCommand As DelegateCommand(Of TheRecordType) = new DelegateCommand(Of TheRecordType)(Sub(r) SelectedRecord = r)
This was attempted with various options for the SelectedItem binding mode.
If the DataGrid SelectedItem binding is removed, We get 1,2,4,5,6, and 7. but selecting the record from the combo box would not show the record as selected in the grid.
If the DataGrid SelectedItem binding is set to OneWay, Selecting a record via the combo box breaks: Setting SelectedRecord triggers the SelectionChanged event in the DataGrid, which uses the value before the event and effectively sets everything back to the original value.
This can be remedied by introducing a sentinal on the Set of the Property in the ViewModel
Private _selectedRecord As TheRecordType
Private _enableRecordSelection As Boolean = true
Public Property SelectedRecord As TheRecordType
Get
Return _selectedRecord
End Get
Set(value As TheRecordType)
If _enableRecordSelection
_enableRecordSelection = false
SetProperty(_selectedRecord , value)
_enableRecordSelection = true
End If
End Set
End Property
This actually works, and we came up with it while writing the question, but feels horribly hacky. My gut is telling me there has to be a better way so I'm still asking:
Is there a clean (preferably xaml only) way to set this up?
The other most successful things we tried:
Straight xaml configuration for the DataGrid with TwoWay binding
<DataGrid x:Name="TheDataGrid"
ItemsSource="{Binding Source={StaticResource GridRecords}}"
SelectedItem="DataContext.SelectedRecord, ElementName=LayoutRoot, Mode=TwoWay}"/>
With this, we satisfy requirements 1 through 6; however when selecting the record through the grid, the previous record is always highlighted instead of the current one.
DataGrid.InputBindings
<DataGrid.InputBindings>
<MouseBinding Gesture="LeftClick"
CommandParameter="{Binding SelectedItem, ElementName=TheDataGrid}"
Command="{Binding DataContext.SelectRecordFromGridCommand, ElementName=LayoutRoot}"/>
</DataGrid.InputBindings>
With no SelectedItem binding, this behaves similarly to the no binding InteractionTrigger on SelectionChanged, except it requires the user to perform multiple mouse actions. A first click selects the row in the grid (actual bold blue selection) The second click triggers the Command.
With a OneWay binding on SelectedItem, this behaves similarly to the straight xaml config, again except needing to click multiple times.
Again to reiterate the question:
Is there a cleaner way to accomplish the 7 requirements than to resort to the sentinal value on the property setter?
According to you ask, I understand that you want to sync the selected item in Datagrid and ComboBox. If I were you, I will use the that two control binding the same object(SelectedRecord). I only familiar with C#, so the code is write in C#. Hope it can help you.
For XAML:
<DataGrid ItemsSource="{Binding Records}"
SelectedValue="{Binding SelectedRecord}" />
<ComboBox Grid.Column="1" ItemsSource="{Binding Records}"
DisplayMemberPath="Id"
SelectedValue="{Binding SelectedRecord}" />
For ViewModel:
public ObservableCollection<Record> Records { get; } = new ObservableCollection<Record>();
public Record SelectedRecord
{
get { return _selectedRecord; }
set
{
_selectedRecord = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public class Record
{
private static int id = 0;
public Record()
{
Id = ++id;
}
public int Id { get; set; }
}

how to get muliplte checked datagrid checkbox items with MVVM

I have datagrid which consist of muliple data grid checkboxes,i want to get the datagrid checked items,i am able to get only the single selected row item,but i need collection of checked checkboxes, below code i am using .Please let me know how to resolve this
**Xaml*****
<DataGrid SelectedItem="{Binding SelectedRow, Mode=TwoWay}" ItemsSource="{Binding ManualDataTable}" Background="{Binding ElementName=gd,Path=Background}">
<DataGrid.Columns>
<DataGridCheckBoxColumn Binding="{Binding UserID}" Width="60" />
<DataGridTextColumn Binding="{Binding Name}" Width="140" Header="Name" FontSize="16" FontFamily="segoe_uilight" IsReadOnly="True" />
</DataGrid.Columns>
</DataGrid>
<Button BorderBrush="{x:Null}" Content="Add participants" Width="220" Height="50" FontSize="20" Command="{Binding SaveAssignedUser}"/>
*****View Model***********
DataTable _manualDataTable;
public DataTable ManualDataTable
{
get
{
return _manualDataTable;
}
set
{
_manualDataTable = value;
RaisePropertyChanged("ManualDataTable");
}
}
private List<DataRowView> selectedRow;
public List<DataRowView> SelectedRow
{
get
{
return selectedRow;
}
set
{
selectedRow = value;
RaisePropertyChanged(() => SelectedRow);
}
}
public void ExecuteSaveAssignedUser()
{
SelectedRow = new List<DataRowView>();**///need multiple checked checkboxes collection**
foreach (DataRowView drv in SelectedRow)
{
}
}
Your checkbox column is bound to a a property called UserID, which I guess is the name of a column in your DataTable. Whenever you check/uncheck a checkbox in the datagrid, the binding will change the value of UserID to true or false, in the relevant DataRow of the DataTable.
Why would you bind a checkbox column to a user ID? I suspect this isn't what you actually want. Instead you probably need to add a boolean column to your DataTable (e.g. IsSelected), and bind your checkbox column to that instead.
It looks like you are using MVVM, so it isn't possible to access the datagrid items from within your view-model. You can only access the data that the grid is bound to (i.e. your DataTable). Your view-model code needs to iterate through the rows in this DataTable, examining the value of the UserID column (or the "IsSelected" column if you add one!) to determine whether that row's checkbox is checked in the datagrid.
The SelectedItem property that you are binding to is completely unrelated to your checkbox column. Checking and unchecking these will have no effect on SelectedItem. This property is used to determine which row the user has selected with the mouse (which gives the row a different b/g colour). Forget about this property - it's not relevant to what you are trying to do.
As already mentioned elsewhere, try and avoid DataTables in WPF. Instead, define some kind of "User" class, and have your view-model expose a collection of these for your grid to bind to.
You seem to be missing the entire point of data binding... that is that you have access to all of the data that is displayed in your DataGrid from your code behind. You have bound the ManualDataTable DataTable property to the DataGrid, so the values that are data bound to the RadioButton controls are all in one column of your DataTable.
Therefore, all you need to do to access them is to look in your DataTable. There are several ways to achieve this, but here is one:
foreach(DataRow row in ManualDataTable.Rows)
{
if (row[requiredColumnIndex] == true) AddRowToSomeCollection(row);
}
However, if you're going to continue to use WPF, I'd seriously advise that you stop using these old classes, such as DataTables. Generally in WPF, we define custom classes and that makes everything much simpler in the long run.

How can I perform a custom action on the data of a DataGridRow when it is selected?

I've been trying to figure out how to get this custom behaviour into a datagrid with out having much look when searching online for solutions.
Given the following datagrid (some xaml removed for brevity):
<DataGrid ItemsSource="{Binding Items}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTemplateColumn Width="auto">
<DataGridTemplateColumn.HeaderTemplate>
<DataTemplate>
<CheckBox />
</DataTemplate>
</DataGridTemplateColumn.HeaderTemplate>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding Selected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
I have the checkbox successfully bound to the databound object for each row. (Note: I'm using a DataGridTemplateColumn rather than DataGridCheckBoxColumn so that you do not need to double-click to change the value).
What I would like to achieve is to have the ability to tick the checkbox / update the Selected property of the databound object when the user selects a row. Effectively making the entire row click set the checked property of the checkbox. Ideally, I'd like to do this without a code behind file if possible as I'm trying to keep my code behinds as clean as possible.
Another feature which I would like, if possible, would be that clicking on a row would toggle it's selected property so that if you click on another one, the previous one stays selected as well as the new one.
Any help is much appreciated.
For clarity. I understood
Another feature which I would like, if possible, would be that
clicking on a row would toggle it's selected property so that if you
click on another one, the previous one stays selected as well as the
new one.
in the way, that you want the CheckBox of the an item, respectively the Selected property on the items ViewModel, to stay selected, when the next DataGridRow is selected, but not the DataGridRow itself? Is that correct?
My suggestion is to extend the behavior of your DataGrid using *WPF behavior*s (This is a good introduction. This way you can keep your codebehind clear, but don't have to twist XAML to make it do what you want.
This is basically the idea of behaviors: Writing testable code, which is not coupled to your concrete view, but nonetheless allowing you to write complicated stuff in 'real' code and not in XAML. In my opinion your case is a typical task for behaviors.
Your behavior could look about as simple as this.
public class CustomSelectionBehavior : Behavior<DataGrid>
{
protected override void OnAttached()
{
// Set mode to single to be able to handle the cklicked item alone
AssociatedObject.SelectionMode = DataGridSelectionMode.Single;
AssociatedObject.SelectionChanged += AssociatedObject_SelectionChanged;
}
protected override void OnDetaching()
{
AssociatedObject.SelectionChanged -= AssociatedObject_SelectionChanged;
}
private void AssociatedObject_SelectionChanged(object sender, SelectionChangedEventArgs args)
{
// Get DataContext of selected row
var item = args.AddedItems.OfType<ItemViewModel>();
// Toggle Selected property
item.Selected = !item.Selected;
}
}
Attaching the behavior to your specific DataGrid, is done in XAML:
<DataGrid ...>
<i:Interaction.Behaviors>
<b:CustomSelectionBehavior />
</i:Interaction.Behaviors>
...
</DataGrid>
You need to reference
System.Windows.Interactivity.dll
which contains the Behavior<T> baseclass as well.

databinding and focus coordination

I have several controls including a DataGrid that I want to be disabled until there is a valid value in the first TextBox in the presentation. So I added a boolean property to bind to in the VM and bind to it in the xaml (below).
The binding works, but has the side effect of 'trapping' the user in the TextBox (MoneyToAllocate).
Presumably this is because the TB binding is LostFocus and there is no place for the focus to go and actually trigger the updates. What's a good way to fix this?
Cheers,
Berryl
ViewModel
public bool HasMoneyToAllocate { get { return MoneyToAllocate.Amount > 0; } }
public Money MoneyToAllocate {
get { return _moneyToAllocate; }
set {
if (value.Amount < 0) return;
_moneyToAllocate = new Money(value.Amount, SelectedCurrency);
NotifyPropertyChanged(() => HasMoneyToAllocate);
}
}
View
<TextBox Text="{Binding MoneyToAllocate, Converter={StaticResource moneyConverter}}" />
<DataGrid IsEnabled="{Binding HasMoneyToAllocate}" ...
EDIT
I should have added that I tried PropertyChanged for update but it gets a bit messy since the value of the text box needs to be formatted by the converter. Any other ideas?
FINAL EDIT
I wound up letting another control that previously wasn't a tab stop be a tab stop, so the text box had a place to go. Phil understood the problem best and gets the answer, even though the range of values the user can input (.001 to decimal.MaxValue) make an up-down impractical.
Use UpdateSourceTrigger=PropertyChanged
<TextBox
Text="{Binding MoneyToAllocate, UpdateSourceTrigger=PropertyChanged,
Converter={StaticResource moneyConverter}}" />
Then you have to use UpdateSourceTrigger=PropertyChanged
- if you use that binding you are using the value in the VM will not effected till the focus moves from the textBox
- but if you add UpdateSourceTrigger=PropertyChanged to your binding the VM property (MoneyToAllocate) will effected immediately (when the textBox.Text value changed)

XAML ReadOnly ComboBox

To set up a ReadOnly ComboBox in XAML (WPF), you have to set up a ComboBox and a TextBox showing only one of them according to a pair of properties IsReadOnly/IsEditable that must exist on your ViewModel. Note that on this sample "UserNVL" must exist in the resources and it should be a NameValueList collection that allows us to convert ID to names. In this case the RecipientID is the key for a user name. Note also the VisibilityConverter must also exist in the resources and it's a standard BooleanToVisibilityConverter.
Gosh! This was so hard to find I had to made it myself. This allows the user the select the content of the text box. No way a disabled ComboBox would ever allow you to do it.
There are two properties named IsHitTestVisible & IsTabVisible. the former makes the control deaf to mouse events and the latter to keyboard events.
This could help you as it would not give the disabled look to your combo box but you will succeed in making a read only combo box..
Source :-
http://www.telerik.com/community/forums/wpf/combobox/isreadonly-does-seem-to-work.aspx
Why not just set IsEnabled=false?
<DockPanel>
<TextBlock Text="Recipient" Margin="6,9,3,6" HorizontalAlignment="Right"/>
<ComboBox
x:Name="RecipientID"
ItemsSource="{Binding Source={StaticResource UserNVL}}"
DisplayMemberPath="Value"
SelectedValuePath="Key"
SelectedValue="{Binding Path=RecipientID}"
Height="20"
Margin="6,6,0,6"
MinWidth="200"
HorizontalAlignment="Left"
IsEditable ="True"
Visibility="{Binding Path=IsEditable, Converter={StaticResource VisibilityConverter}}"/>
<TextBox
x:Name="RecipientName"
Text="{Binding ElementName=RecipientID, Path=Text}"
Margin="6,6,0,6"
MinWidth="200"
HorizontalAlignment="Left"
Style="{StaticResource textBoxInError}"
Visibility="{Binding Path=IsReadOnly, Converter={StaticResource VisibilityConverter}}"/>
</DockPanel>
I think that you will find it much easier and practical to create a class to extend the ComboBox class in this very simple manner:
override the OnSelectionChanged method of the Combobox to check the property IsReadOnly before to allow base.OnSelectionChanged(e) to run.
That way you just have to set ComboBox.IsReadOnly property to True. No big XAML to write everywhere...
Here is a custom ComboBox subclass that gives the read only behaviour I needed for my scenario:
public class ReadOnlyComboBox : ComboBox
{
static ReadOnlyComboBox()
{
IsDropDownOpenProperty.OverrideMetadata(typeof(ReadOnlyComboBox), new FrameworkPropertyMetadata(
propertyChangedCallback: delegate { },
coerceValueCallback: (d, value) =>
{
if (((ReadOnlyComboBox)d).IsReadOnly)
{
// Prohibit opening the drop down when read only.
return false;
}
return value;
}));
IsReadOnlyProperty.OverrideMetadata(typeof(ReadOnlyComboBox), new FrameworkPropertyMetadata(
propertyChangedCallback: (d, e) =>
{
// When setting "read only" to false, close the drop down.
if (e.NewValue is true)
{
((ReadOnlyComboBox)d).IsDropDownOpen = false;
}
}));
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
if (IsReadOnly)
{
// Disallow changing the selection when read only.
e.Handled = true;
return;
}
base.OnSelectionChanged(e);
}
}
Points about this approach:
Doesn't break any existing styles applied to the element, unlike an approach that introduces additional UI elements.
Doesn't break input focus while read only. You can still tab into and click to focus this element. This is more accessible, which is a concern in my scenario.
The UI element doesn't, but default, look any different when read only. If you need that, you would have to apply relevant styles to make it so.
If IsEnabled is set to false, Combobox value is nearly not readable. What I found as suitable solution is:
combobox and textbox (formated as readonly) are in the same grid position
combobox spans to next column to gain additional 15 width so dropdown button is visible
textbox.IsVisible is bound to combobox.IsEnabled with bool to visibility converter.
textbox.Text is bound to combobox.SelectedItem (in my case it is strongly typed so I actually bound into .DisplayText of it)

Resources