I'm trying to access radio buttons inside datatemplate which is in a datagrid. I came across a lot of answers but none work for me.
I have 3 different radio buttions in 3 different columns. i just cant figure out how to access those radio buttons and its killing me, i'v been struggling with it for days PLEASE HELP.
<DataGridTemplateColumn Header="Yes" Width="Auto">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Opacity="0.8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<RadioButton x:Name="rbYes" GroupName="{Binding QuestionID}" Content="Yes" Grid.Column="0" />
</Grid>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="No" Width="Auto">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Opacity="0.8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<RadioButton x:Name="rbNo" GroupName="{Binding QuestionID}" Content="No" Grid.Column="0" />
</Grid>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="N/A" Width="Auto">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Opacity="0.8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<RadioButton x:Name="rbNA" GroupName="{Binding QuestionID}" Content="N/A" Grid.Column="0" />
</Grid>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
And here is my code behind. I have changed it so many times.
RadioButton rbYes = (RadioButton)FindName("rbYes");
RadioButton rbNo = (RadioButton)FindName("rbNo");
RadioButton rbNA = (RadioButton)FindName("rbNA");
if (rbYes.IsChecked == true)
{
rbNo.IsChecked = false;
rbNA.IsChecked = false;
int questionID = Convert.ToInt32(drv["QuestionID"]);
questionAnswer = "Yes".ToString();
if (drv["Notes"].ToString() == "")
{
string notes = null;
}
else
{
string notes = drv["Notes"].ToString();
}
Assuming that you are familiar with data binding (as you have already bound the QuestionID) my suggestion is as follows;
Have 3 boolean properties say IsYes, IsNo, IsNA in the object through which the data is populated in the datagrid. Make the collection that is bound to the ItemSource of your datagrid as an ObservableCollection.
Bind the IsChecked of the radio buttons to these three properties respectively.
By doing the above steps, you don't have to worry about going deep into the cells of your datagrid to find the controls and to know whether they are set or not.
[UPDATE] Sorry for the delay. I was in a meeting. The Code is as follows.
Please note that this is a very rough mock up of how to achieve the concept which I mentioned above.
MainWindow.xaml
<Window x:Class="testWPFApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:testWPFApplication"
mc:Ignorable="d" DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="MainWindow" Height="350" Width="525">
<Grid x:Name="LayoutRoot" >
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="30" />
</Grid.RowDefinitions>
<DataGrid Grid.Row="0" SelectionMode="Single"
ItemsSource="{Binding Questions}" SelectedItem="{Binding SelectedQuestion,Mode=TwoWay}" AutoGenerateColumns="False"
CanUserAddRows="False" CanUserDeleteRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Question Id" Binding="{Binding QuestionId}" IsReadOnly="True" />
<DataGridTextColumn Header="Question" Binding="{Binding QuestionDescription}" Width="3*" IsReadOnly="True" />
<DataGridTemplateColumn Header="Yes">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<RadioButton GroupName="{Binding QuestionId}" IsChecked="{Binding IsYes, Mode=TwoWay,UpdateSourceTrigger=LostFocus,NotifyOnTargetUpdated=True}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="No">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<RadioButton GroupName="{Binding QuestionId}" IsChecked="{Binding IsNo, Mode=TwoWay,UpdateSourceTrigger=LostFocus,NotifyOnTargetUpdated=True}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="N/A">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<RadioButton GroupName="{Binding QuestionId}" IsChecked="{Binding IsNA, Mode=TwoWay,UpdateSourceTrigger=LostFocus,NotifyOnTargetUpdated=True}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Button Grid.Row="1" Width="100" x:Name="btnCheckAnswers" HorizontalAlignment="Right" Margin="5" Click="btnCheckAnswers_Click" Content="Check Answers" />
</Grid>
</Window>
MainWindow.xaml.cs (code behind. can be moved a different ViewModel class)
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace testWPFApplication
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
private ObservableCollection<Question> _questions;
private Question _selectedQuestion;
public ObservableCollection<Question> Questions
{
get { return _questions; }
set
{
_questions = value;
OnPropertyChanged("Questions");
}
}
public Question SelectedQuestion
{
get { return _selectedQuestion; }
set
{
_selectedQuestion = value;
OnPropertyChanged("SelectedQuestion");
OnSelectedQuestionChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string strPropertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(strPropertyName));
}
}
public MainWindow()
{
InitializeComponent();
DataContext = this;
Init();
}
private void Init()
{
CreateMockData();
}
private void CreateMockData()
{
if (Questions == null)
{
Questions = new ObservableCollection<Question>();
}
else
{
Questions.Clear();
}
for (int i = 1; i <= 5; i++)
{
Question q = new Question
{
QuestionId = i,
QuestionDescription = "Sample Question " + i.ToString(),
IsYes = false,
IsNA = false,
IsNo = false
};
Questions.Add(q);
}
}
private void OnSelectedQuestionChanged()
{
if(SelectedQuestion != null)
{
}
}
private void btnCheckAnswers_Click(object sender, RoutedEventArgs e)
{
StringBuilder strQuestionAnswer = new StringBuilder();
foreach (Question item in Questions)
{
strQuestionAnswer.AppendLine(item.QuestionDescription + ":");
strQuestionAnswer.AppendLine("IsYes:" + item.IsYes);
strQuestionAnswer.AppendLine("IsNo:" + item.IsNo);
strQuestionAnswer.AppendLine("IsNA:" + item.IsNA);
}
MessageBox.Show(strQuestionAnswer.ToString());
}
}
public class Question
{
public int QuestionId { get; set; }
public string QuestionDescription { get; set; }
public bool IsYes { get; set; }
public bool IsNo { get; set; }
public bool IsNA { get; set; }
}
}
Window Load
Few values set
Result when the button is clicked
Hope this example helps you to address the issue that you have.
Thank you for answering so quickly. But thats not the problem, the problem is, I'm try to access the radiobuttons im using but i can find the buttons with the FindName function. Null is returned for rbYes, rbNo and rbNA. I don't know if my explanation is good enough.
Related
First, let me apologize for the length of this post - I know it's long, but I figured for this one, more detail is better than less.
What I'm trying to achieve now is a totals footer row for a datagrid. Since it needs to show up on the bottom row, the approach I'm taking is to just add some TextBlocks that line up with the columns in the datagrid.
My app has multiple datagrids inside an ItemsControl so I haven't found a nice way of just setting a binding. Using RelativeSource doesn't seem to be an option as there's no way (as far as I can tell) to point it at a descendent element, then search for a particular child. So instead I've written a bit of hackery to do what I want in code behind.
Ok, so now the problem.. Everything looks fine when the app starts, but as soon as any of the items in the grid change, the width binding seems to break completely. I wrote a small test app to show what I mean. Here's some screenshots:
Now if I click the button to change an element, the width binding of the footer textblocks breaks:
I'm completely stumped as to the cause of this behavior. I'm pretty new to WPF and just muddling my way through building my first app. So if this is a dumb way of doing things, please let me know. Here's my code.
MainWindow.xaml:
<Window x:Class="WpfTestApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfTestApp"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<StackPanel Orientation="Vertical">
<ItemsControl x:Name="BarsItemsControl" ItemsSource="{Binding Bars}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Description}" />
<DataGrid x:Name="FooGrid"
ItemsSource="{Binding Foos}"
IsSynchronizedWithCurrentItem="False"
AutoGenerateColumns="False"
SelectionUnit="Cell"
SelectionMode="Extended"
CanUserReorderColumns="False"
CanUserAddRows="True"
HeadersVisibility="Column">
<DataGrid.Columns>
<DataGridTextColumn Header="Col 1" Width="*" Binding="{Binding Value1}" />
<DataGridTextColumn Header="Col 2" Width="*" Binding="{Binding Value2}" />
<DataGridTextColumn Header="Col 3" Width="*" Binding="{Binding Value3}" />
</DataGrid.Columns>
</DataGrid>
<StackPanel x:Name="TotalsRow" Orientation="Horizontal">
<TextBlock Text="{Binding Totals[0]}" />
<TextBlock Text="{Binding Totals[1]}" />
<TextBlock Text="{Binding Totals[2]}" />
</StackPanel>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="Change something" Click="Button_Click" />
</StackPanel>
</Window>
MainWindow.xaml.cs
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace WpfTestApp
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public IList<Bar> Bars { get; }
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
this.Bars = new ObservableItemsCollection<Bar>();
var foos = new ObservableItemsCollection<Foo>();
for (int i = 0; i < 5; i++)
{
foos.Add(new Foo()
{
Value1 = 14.23,
Value2 = 53.23,
Value3 = 35.23
});
}
var foos2 = new ObservableItemsCollection<Foo>();
for (int i = 0; i < 5; i++)
{
foos2.Add(new Foo()
{
Value1 = 14.23,
Value2 = 53.23,
Value3 = 35.23
});
}
this.Bars.Add(new Bar(foos)
{
Description = "Bar 1",
});
this.Bars.Add(new Bar(foos2)
{
Description = "Bar 2",
});
this.Loaded += MainWindow_Loaded;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
// Bind widths of the TotalsRow textblocks (footers) to the width of the
// datagrid column they're associated with
var elements = new List<FrameworkElement>();
this.GetChildElementsByName(this.BarsItemsControl, "FooGrid", ref elements);
foreach (var element in elements)
{
var dataGrid = element as DataGrid;
if (dataGrid != null)
{
var totalsRowList = new List<FrameworkElement>();
this.GetChildElementsByName(VisualTreeHelper.GetParent(dataGrid), "TotalsRow", ref totalsRowList);
if (totalsRowList.Count > 0)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(totalsRowList[0]); i++)
{
var textBlock = VisualTreeHelper.GetChild(totalsRowList[0], i) as TextBlock;
Binding widthBinding = new Binding();
widthBinding.Source = dataGrid.Columns[i];
widthBinding.Path = new PropertyPath("ActualWidth");
widthBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
BindingOperations.SetBinding(textBlock, TextBlock.WidthProperty, widthBinding);
}
}
}
}
}
/// <summary>
/// Populate a list of elements in the visual tree with the given name under the given parent
/// </summary>
public void GetChildElementsByName(DependencyObject parent, string name, ref List<FrameworkElement> elements)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
var element = child as FrameworkElement;
if (element != null && element.Name == name)
{
elements.Add(element);
}
GetChildElementsByName(child, name, ref elements);
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
this.Bars[0].Foos[3].Value1 = 10;
}
}
}
Foo.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace WpfTestApp
{
public class Foo : INotifyPropertyChanged
{
private double value1;
public double Value1 {
get { return value1; }
set { value1 = value; OnPropertyChanged(); }
}
private double value2;
public double Value2
{
get { return value2; }
set { value2 = value; OnPropertyChanged(); }
}
private double value3;
public double Value3
{
get { return value3; }
set { value3 = value; OnPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
}
Bar.cs
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
namespace WpfTestApp
{
public class Bar : INotifyPropertyChanged
{
public Bar(ObservableItemsCollection<Foo> foos)
{
this.Foos = foos;
this.Totals = new double[3] { 14, 14, 14};
this.Foos.CollectionChanged += Foos_CollectionChanged;
}
private void Foos_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
//var fooList = this.Categories.Cast<CategoryViewModel>();
this.Totals[0] = this.Foos.Sum(f => f.Value1);
this.Totals[1] = this.Foos.Sum(f => f.Value2);
this.Totals[2] = this.Foos.Sum(f => f.Value3);
OnPropertyChanged(nameof(Totals));
}
public string Description { get; set; }
public ObservableItemsCollection<Foo> Foos { get; }
public double[] Totals { get; }
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
}
ObservableItemsCollection.cs
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
namespace WpfTestApp
{
public class ObservableItemsCollection<T> : ObservableCollection<T>
where T : INotifyPropertyChanged
{
private void Handle(object sender, PropertyChangedEventArgs args)
{
base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset, null));
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (object t in e.NewItems)
{
((T)t).PropertyChanged += Handle;
}
}
if (e.OldItems != null)
{
foreach (object t in e.OldItems)
{
((T)t).PropertyChanged -= Handle;
}
}
base.OnCollectionChanged(e);
}
}
}
Just bind to the ActualWidth of the columns:
<Window x:Class="WpfTestApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfTestApp"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<StackPanel Orientation="Vertical">
<ItemsControl x:Name="BarsItemsControl" ItemsSource="{Binding Bars}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Description}" />
<DataGrid x:Name="FooGrid"
ItemsSource="{Binding Foos}"
IsSynchronizedWithCurrentItem="False"
AutoGenerateColumns="False"
SelectionUnit="Cell"
SelectionMode="Extended"
CanUserReorderColumns="False"
CanUserAddRows="True"
HeadersVisibility="Column">
<DataGrid.Columns>
<DataGridTextColumn x:Name="col1" Header="Col 1" Width="*" Binding="{Binding Value1}" />
<DataGridTextColumn x:Name="col2" Header="Col 2" Width="*" Binding="{Binding Value2}" />
<DataGridTextColumn x:Name="col3" Header="Col 3" Width="*" Binding="{Binding Value3}" />
</DataGrid.Columns>
</DataGrid>
<StackPanel x:Name="TotalsRow" Orientation="Horizontal">
<TextBlock Width="{Binding ElementName=col1, Path=ActualWidth}" Text="{Binding Totals[0]}" />
<TextBlock Width="{Binding ElementName=col2, Path=ActualWidth}" Text="{Binding Totals[1]}" />
<TextBlock Width="{Binding ElementName=col3, Path=ActualWidth}" Text="{Binding Totals[2]}" />
</StackPanel>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="Change something" Click="Button_Click" />
</StackPanel>
Change your last stackpanel to grid in your xaml source:
<Grid x:Name="TotalsRow">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Totals[0]}" Grid.Column="0"/>
<TextBlock Text="{Binding Totals[1]}" Grid.Column="1"/>
<TextBlock Text="{Binding Totals[2]}" Grid.Column="2"/>
</Grid>
Result:
Here is my goal (simplified) : Create a userControl to display readable bitField.
Exemple with value 0x3: want to display option1 (bit 1): enable, option2 (bit 2):enable, option3 (bit 3): disable ...
userControl behavior: if I click on this control, it's open a popup (like combobox) with checkBoxes which allow you to enable optionX (which will change the value and displayed text).
This is the source code I use for the UserControl view
<UserControl x:Name="userControl" x:Class="MyProg.Views.BitField"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MyProg.Views"
mc:Ignorable="d"
>
<Grid x:Name="LayoutRoot" Height="{Binding ActualHeight, ElementName=userControl, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" Width="{Binding ActualWidth, ElementName=userControl, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ToggleButton x:Name="TogglePopupButton" Background="Transparent" BorderBrush="Transparent" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>
<Label Content="{Binding Path=Text}" Grid.Column="0" VerticalAlignment="Center" Margin="0,-5,0,-5" />
<Path x:Name="Arrow" Grid.Column="1" Fill="Black" VerticalAlignment="Center" Data="M0,0 L0,2 L4,6 L8,2 L8,0 L4,4 z" HorizontalAlignment="Right"/>
</Grid>
</ToggleButton>
<Popup x:Name="ToggledPopup" StaysOpen="False" IsOpen="{Binding IsChecked, ElementName=TogglePopupButton, Mode=TwoWay}" Width="{Binding ActualWidth, ElementName=userControl, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" Grid.Row="1">
<Border Background="White" BorderThickness="1" BorderBrush="Black">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<CheckBox Content="option 1" Grid.Column="0" />
<CheckBox Content="option 2" Grid.Column="2" />
<CheckBox Content="option 3" Grid.Column="4" />
</Grid>
</ScrollViewer>
</Border>
</Popup>
</Grid>
</UserControl>
The code behind for binding value
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using MyProg.ViewModels;
namespace MyProg.Views
{
/// <summary>
/// Interaction logic for BitField.xaml
/// </summary>
public partial class BitField : UserControl
{
internal vmBitField m_ViewModel;
public BitField()
{
InitializeComponent();
// Load the viewModel
m_ViewModel = new vmBitField();
LayoutRoot.DataContext = m_ViewModel;
m_ViewModel.PropertyChanged += M_ViewModel_PropertyChanged;
}
private void M_ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Value")
{
if(IntValue != m_ViewModel?.Value)
IntValue = (int)m_ViewModel.Value;
}
}
/// <summary>
/// Gets or sets the value which is displayed
/// </summary>
public int IntValue
{
get { return (int)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value);}
}
/// <summary>
/// Identified the Value dependency property
/// </summary>
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("IntValue", typeof(int), typeof(BitField), new PropertyMetadata(0, OnIntValueSet));
private static void OnIntValueSet(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((BitField)d).m_ViewModel.Value = (uint?)(int)e.NewValue;
}
}
}
and the UserControl viewModel
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MyProg.MVVM;
namespace MyProg.ViewModels
{
class vmBitField : ViewModelBase
{
#region Members
protected uint m_Value = 0; // 7
#endregion
#region Properties
public uint? Value { get { return m_Value; } set { if (value != m_Value) { m_Value = (uint)value; RaiseEventPropertyChanged("Option1"); RaiseEventPropertyChanged("Option2"); RaiseEventPropertyChanged("Option3"); RaiseEventPropertyChanged("Text"); } } }
public bool Option1 { get {return (Value & 0x1) == 1; } set { if (value != Option1) { m_Value ^= 0x1; RaiseEventPropertyChanged("Option1"); RaiseEventPropertyChanged("Value"); RaiseEventPropertyChanged("Text"); } } }
public bool Option2 { get { return (Value & 0x2) == 1; } set { if (value != Option2) { m_Value ^= 0x2; RaiseEventPropertyChanged("Option2"); RaiseEventPropertyChanged("Value"); RaiseEventPropertyChanged("Text"); } } }
public bool Option3 { get { return (Value & 0x4) == 1; } set { if (value != Option3) { m_Value ^= 0x4; RaiseEventPropertyChanged("Option3"); RaiseEventPropertyChanged("Value"); RaiseEventPropertyChanged("Text"); } } }
public string Text { get { return "Option1:" + Option1 + " Option2:" + Option2 + " Option3:" + Option3; } }
#endregion
#region Contructors
public vmBitField()
{
}
#endregion
#region Methodes
#endregion
}
}
This works correctly when I insert this userControl in a page or windows.
<local:LockType IntValue="{Binding Path=ValueAsInt, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
Now, I have a list with lot of bitfield. I want to display all of this in a datagrid. So I want to insert this userControl in a dataGrid.
This is the code to add the datagrid
<DataGrid Grid.Row="13" Grid.ColumnSpan="3" AutoGenerateColumns="False" Name="dataGrid1" AlternationCount="2"
ItemsSource="{Binding Values}"
CanUserAddRows="False" CanUserDeleteRows="False" CanUserResizeRows="False" CanUserReorderColumns="False" >
<DataGrid.Columns>
<DataGridTemplateColumn Header="Id" HeaderStyle="{StaticResource DataGridHeaderCenter}"
CellTemplate="{StaticResource IdText}"/>
<DataGridTemplateColumn Header="Value" Width="400" HeaderStyle="{StaticResource DataGridHeaderCenter}" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<local1:LockType IntValue="{Binding Path=ValueAsInt, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="CurrentValue"
CellTemplate="{StaticResource CurrentValueText}" />
</DataGrid.Columns>
</DataGrid>
When my usercontrol is in a datagrid, when I click on togglebuton, it's open the popup correctly, but then I can't interact (check or uncheck) with any of checkBoxes inside the popup.
Anyone could help me to find why checkboxes inside a popup doesn't fired event ?
I could add more info if needed.
Best Regards
JM
You will have to set the FocusManager.IsFocusScope of the popup to true.
You can also set the cell's IsFocussable property through a style setter.
<Style TargetType="{x:Type DataGridCell}"> <Setter Property="Focusable" Value="False"></Setter> </Style>
The problem why it does not let you edit anything on the popup is because the cell editing has not ended. Hence, you can also do the following by registering to the DataGrid_CellEditEnding event.
private static void DataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
if (e.Column.GetType() == typeof(DataGridTemplateColumn))
{
var popup = GetVisualChild<Popup>(e.EditingElement);
if (popup != null && popup.IsOpen)
{
e.Cancel = true;
}
}
}
I'm in search of some help. I've created a very basic MVVM setup. My object is called VNode which has the properties Name,Age,Kids. What I want to happen is when the user selects VNodes on the left, it displays their more in depth data on the right as scene in the image below. I'm not sure how to go about doing this.
image 1: Current
Image 2: Goal
If you don't feel like using the code below to recreate the window you can grab the project solution files from here: DropboxFiles
VNode.cs
namespace WpfApplication1
{
public class VNode
{
public string Name { get; set; }
public int Age { get; set; }
public int Kids { get; set; }
}
}
MainWindow.xaml
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="8" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListBox Grid.Column="0" Background="AliceBlue" ItemsSource="{Binding VNodes}" SelectionMode="Extended">
<ListBox.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="Name: " />
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
</WrapPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" />
<ListBox Grid.Column="2" Background="LightBlue" ItemsSource="{Binding VNodes}">
<ListBox.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
<TextBlock Text=":" FontWeight="Bold" />
<TextBlock Text=" age:"/>
<TextBlock Text="{Binding Age}" FontWeight="Bold" />
<TextBlock Text=" kids:"/>
<TextBlock Text="{Binding Kids}" FontWeight="Bold" />
</WrapPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
MainViewModel.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WpfApplication1
{
public class MainViewModel : ObservableObject
{
private ObservableCollection<VNode> _vnodes;
public ObservableCollection<VNode> VNodes
{
get { return _vnodes; }
set
{
_vnodes = value;
NotifyPropertyChanged("VNodes");
}
}
Random r = new Random();
public MainViewModel()
{
//hard coded data for testing
VNodes = new ObservableCollection<VNode>();
List<string> names = new List<string>() { "Tammy", "Doug", "Jeff", "Greg", "Kris", "Mike", "Joey", "Leslie", "Emily","Tom" };
List<int> ages = new List<int>() { 32, 24, 42, 57, 17, 73, 12, 8, 29, 31 };
for (int i = 0; i < 10; i++)
{
VNode item = new VNode();
int x = r.Next(0,9);
item.Name = names[x];
item.Age = ages[x];
item.Kids = r.Next(1, 5);
VNodes.Add(item);
}
}
}
}
ObservableObject.cs
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace WpfApplication1
{
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
UPDATED
For the sake of example, how about demonstrating if the user just selects a single item in the ListBox on the right, it then displays that selected items more in-depth data on the right as shown in the image below?
There are three and a half answers here. Number one is good general WPF practice that doesn't work in the specific case of ListBox. The second one is a quick and dirty workaround for the problem with ListBox, and the last is the best, because it does nothing in code behind. Least code behind is best code behind.
The first way to do this doesn't require anything of the items you're displaying in the ListBox. They could be strings or integers. If your item type (or types) is a class (or are classes) with a little more meat to it, and you'd like to have each instance know whether it's been selected or not, we'll get to that next.
You need to give your view model another ObservableCollection<VNode> called SelectedVNodes or some such.
private ObservableCollection<VNode> _selectedvnodes;
public ObservableCollection<VNode> SelectedVNodes
{
get { return _selectedvnodes; }
set
{
_selectedvnodes = value;
NotifyPropertyChanged("SelectedVNodes");
}
}
public MainViewModel()
{
VNodes = new ObservableCollection<VNode>();
SelectedVNodes = new ObservableCollection<VNode>();
// ...etc., just as you have it now.
If System.Windows.Controls.ListBox weren't broken, then in your first ListBox, you would bind SelectedItems to that viewmodel property:
<ListBox
Grid.Column="0"
Background="AliceBlue"
ItemsSource="{Binding VNodes}"
SelectedItems="{Binding SelectedVNodes}"
SelectionMode="Extended">
And the control would be in charge of the content of SelectedVNodes. You could also change SelectedVNodes programmatically, and that would update both lists.
But System.Windows.Controls.ListBox is broken, and you can't bind anything to SelectedItems. The simplest workaround is to handle the ListBox's SelectionChanged event and kludge it in the code behind:
XAML:
<ListBox
Grid.Column="0"
Background="AliceBlue"
ItemsSource="{Binding VNodes}"
SelectionMode="Extended"
SelectionChanged="ListBox_SelectionChanged">
C#:
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListBox lb = sender as ListBox;
MainViewModel vm = DataContext as MainViewModel;
vm.SelectedVNodes.Clear();
foreach (VNode item in lb.SelectedItems)
{
vm.SelectedVNodes.Add(item);
}
}
Then bind ItemsSource in your second ListBox to SelectedVNodes:
<ListBox
Grid.Column="2"
Background="LightBlue"
ItemsSource="{Binding SelectedVNodes}">
And that should do what you want. If you want to be able to update SelectedVNodes programmatically and have the changes reflected in both lists, you'll have to have your codebehind class handle the PropertyChanged event on the viewmodel (set that up in the codebehind's DataContextChanged event), and the CollectionChanged event on viewmodel.SelectedVNodes -- and remember to set the CollectionChanged handler all over again every time SelectedVNodes changes its own value. It gets ugly.
A better long-term solution would be to write an attachment property for ListBox that replaces SelectedItems and works right. But this kludge will at least get you moving for the time being.
Update
Here's a second way of doing it, which OP suggested. Instead of maintaining a selected item collection, we put a flag on each item, and the viewmodel has a filtered version of the main item list that returns only selected items. I'm drawing a blank on how to bind VNode.IsSelected to the IsSelected property on ListBoxItem, so I just did that in the code behind.
VNode.cs:
using System;
namespace WpfApplication1
{
public class VNode
{
public string Name { get; set; }
public int Age { get; set; }
public int Kids { get; set; }
// A more beautiful way to do this would be to write an IVNodeParent
// interface with a single method that its children would call
// when their IsSelected property changed -- thus parents would
// implement that, and they could name their "selected children"
// collection properties anything they like.
public ObservableObject Parent { get; set; }
private bool _isSelected = false;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (value != _isSelected)
{
_isSelected = value;
if (null == Parent)
{
throw new NullReferenceException("VNode.Parent must not be null");
}
Parent.NotifyPropertyChanged("SelectedVNodes");
}
}
}
}
}
MainViewModel.cs:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WpfApplication1
{
public class MainViewModel : ObservableObject
{
private ObservableCollection<VNode> _vnodes;
public ObservableCollection<VNode> VNodes
{
get { return _vnodes; }
set
{
_vnodes = value;
NotifyPropertyChanged("VNodes");
NotifyPropertyChanged("SelectedVNodes");
}
}
public IEnumerable<VNode> SelectedVNodes
{
get { return _vnodes.Where(vn => vn.IsSelected); }
}
Random r = new Random();
public MainViewModel()
{
//hard coded data for testing
VNodes = new ObservableCollection<VNode>();
List<string> names = new List<string>() { "Tammy", "Doug", "Jeff", "Greg", "Kris", "Mike", "Joey", "Leslie", "Emily","Tom" };
List<int> ages = new List<int>() { 32, 24, 42, 57, 17, 73, 12, 8, 29, 31 };
for (int i = 0; i < 10; i++)
{
VNode item = new VNode();
int x = r.Next(0,9);
item.Name = names[x];
item.Age = ages[x];
item.Kids = r.Next(1, 5);
item.Parent = this;
VNodes.Add(item);
}
}
}
}
MainWindow.xaml.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
foreach (VNode item in e.RemovedItems)
{
item.IsSelected = false;
}
foreach (VNode item in e.AddedItems)
{
item.IsSelected = true;
}
}
}
}
MainWindow.xaml (partial):
<ListBox
Grid.Column="0"
Background="AliceBlue"
ItemsSource="{Binding VNodes}"
SelectionMode="Extended"
SelectionChanged="ListBox_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="Name: " />
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
</WrapPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" />
<ListBox Grid.Column="2" Background="LightBlue" ItemsSource="{Binding SelectedVNodes}">
<ListBox.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
<TextBlock Text=":" FontWeight="Bold" />
<TextBlock Text=" age:"/>
<TextBlock Text="{Binding Age}" FontWeight="Bold" />
<TextBlock Text=" kids:"/>
<TextBlock Text="{Binding Kids}" FontWeight="Bold" />
</WrapPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Update 2
And here, finally, is how you do it with binding (thanks to OP for figuring out for me how to bind data item properties to ListBoxItem properties -- I should be able to accept his comment as an answer!):
In MainWindow.xaml, get rid of the SelectionCanged event (yay!), and set a Style to do the binding only on the items in the first ListBox. In the second ListBox, that binding will create problems which I'll leave to somebody else to resolve; I have a guess that it might be fixable by fiddling with the order of notifications and assignments in VNode.IsSelected.set, but I could be wildly wrong about that. Anyway the binding serves no purpose in the second ListBox so there's no reason to have it there.
<ListBox
Grid.Column="0"
Background="AliceBlue"
ItemsSource="{Binding VNodes}"
SelectionMode="Extended"
>
<ListBox.Resources>
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</ListBox.Resources>
<ListBox.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="Name: " />
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
</WrapPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
...and I removed the event handler method from the codebehind. But you didn't add it at all, because you're smarter than me and you started with this last version of the answer.
In VNode.cs, VNode becomes an ObservableObject so he can advertise his selection status, and he also fires the appropriate notification in IsSelected.set. He still has to fire the change notification for his Parent's SelectedVNodes property, because the second listbox (or any other consumer of SelectedVNodes) needs to know that the set of selected VNodes has changed.
Another way to do that would be to make SelectedVNodes an ObservableCollection again, and have VNode add/remove himself from it when his selected status changes. Then the viewmodel would have to handle CollectionChanged events on that collection, and update the VNode IsSelected properties when they're added to it or removed from it. If you do that, it's very important to keep the if in VNode.IsSelected.set, to prevent infinite recursion.
using System;
namespace WpfApplication1
{
public class VNode : ObservableObject
{
public string Name { get; set; }
public int Age { get; set; }
public int Kids { get; set; }
public ObservableObject Parent { get; set; }
private bool _isSelected = false;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (value != _isSelected)
{
_isSelected = value;
if (null == Parent)
{
throw new NullReferenceException("VNode.Parent must not be null");
}
Parent.NotifyPropertyChanged("SelectedVNodes");
NotifyPropertyChanged("IsSelected");
}
}
}
}
}
Update 3
OP asks about displaying a single selection in a detail pane. I left the old multi-detail pane in place to demonstrate sharing a template.
That's pretty simple to do, so I elaborated a bit. You could do this only in the XAML, but I threw in a SelectedVNode property in the viewmodel to demonstrate that as well. It's not used for anything, but if you wanted to throw in a command that operated on the selected item (for example), that's how the view model would know which item the user means.
MainViewModel.cs
// Add to MainViewModle class
private VNode _selectedVNode = null;
public VNode SelectedVNode
{
get { return _selectedVNode; }
set
{
if (value != _selectedVNode)
{
_selectedVNode = value;
NotifyPropertyChanged("SelectedVNode");
}
}
}
MainWindow.xaml:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<SolidColorBrush x:Key="ListBackgroundBrush" Color="Ivory" />
<DataTemplate x:Key="VNodeCardTemplate">
<Grid>
<Border
x:Name="BackgroundBorder"
BorderThickness="1"
BorderBrush="Silver"
CornerRadius="16,6,6,6"
Background="White"
Padding="6"
Margin="4,4,8,8"
>
<Border.Effect>
<DropShadowEffect BlurRadius="2" Opacity="0.25" ShadowDepth="4" />
</Border.Effect>
<Grid
x:Name="ContentGrid"
>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<!-- Each gets half of what's left -->
<ColumnDefinition Width="0.5*" />
<ColumnDefinition Width="0.5*" />
</Grid.ColumnDefinitions>
<Border
Grid.Row="0" Grid.RowSpan="3"
VerticalAlignment="Top"
Grid.Column="0"
BorderBrush="{Binding Path=BorderBrush, ElementName=BackgroundBorder}"
BorderThickness="1"
CornerRadius="9,4,4,4"
Margin="2,2,6,2"
Padding="4"
>
<StackPanel Orientation="Vertical">
<StackPanel.Effect>
<DropShadowEffect BlurRadius="2" Opacity="0.25" ShadowDepth="2" />
</StackPanel.Effect>
<Ellipse
Width="16" Height="16"
Fill="DarkOliveGreen"
Margin="0,0,0,2"
HorizontalAlignment="Center"
/>
<Border
CornerRadius="6,6,2,2"
Background="DarkOliveGreen"
Width="36"
Height="18"
Margin="0"
/>
</StackPanel>
</Border>
<TextBlock Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" Text="{Binding Name}" FontWeight="Bold" />
<Separator Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Background="{Binding Path=BorderBrush, ElementName=BackgroundBorder}" Margin="0,3,0,3" />
<!--
Mode=OneWay on Run.Text because bindings on that property should default to that, but don't.
And if you bind TwoWay to a property without a setter, it throws an exception.
-->
<TextBlock Grid.Row="2" Grid.Column="1"><Bold>Age:</Bold> <Run Text="{Binding Age, Mode=OneWay}" /></TextBlock>
<TextBlock Grid.Row="2" Grid.Column="2"><Bold>Kids:</Bold> <Run Text="{Binding Kids, Mode=OneWay}" /></TextBlock>
</Grid>
</Border>
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding}" Value="{x:Null}">
<Setter TargetName="ContentGrid" Property="Visibility" Value="Hidden" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<!-- I think this should be the default, but it isn't. -->
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</Window.Resources>
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="8" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="0.5*" />
<RowDefinition Height="0.5*" />
</Grid.RowDefinitions>
<ListBox
x:Name="VNodeMasterList"
Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="2"
Background="{StaticResource ListBackgroundBrush}"
ItemsSource="{Binding VNodes}"
SelectionMode="Extended"
SelectedItem="{Binding SelectedVNode}"
>
<ListBox.Resources>
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</ListBox.Resources>
<ListBox.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="Name: " />
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
</WrapPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<GridSplitter Grid.Column="1" Grid.RowSpan="2" Grid.Row="0" Width="5" HorizontalAlignment="Stretch" />
<Border
Grid.Column="2"
Grid.Row="0"
Background="{StaticResource ListBackgroundBrush}"
>
<ContentControl
Content="{Binding ElementName=VNodeMasterList, Path=SelectedItem}"
ContentTemplate="{StaticResource VNodeCardTemplate}"
/>
</Border>
<ListBox
Grid.Column="2"
Grid.Row="1"
Background="{StaticResource ListBackgroundBrush}"
ItemsSource="{Binding SelectedVNodes}"
ItemTemplate="{StaticResource VNodeCardTemplate}"
/>
</Grid>
</Window>
I've added a binding to an IEnumerable collection, that's populated from a async method call. The data is retrieved from a remote database and then added to the CustomerOrders list.
But after running the application, my UI binding isn't showing on the view. The view shows no data.
In order to debug the issue, I checked the following:
Checked binding and data context by binding to a static list of data.
Debugged the CustomerOrders list, after the data call, which shows as being populated after the method returns.
I also checked the thread name, and it shows as being a "main thread". (Not sure if that could be the reason as it's a different thread.)
3.1.I also implemented native INPC on the CustomerOrders property, and set a breakpoint on set which shows the list is populated. See snapshot.
Does anyone have suggestions on what the issue could be here?
The is a summary of CustomerOrderViewModel, set up as follows. A Task property, Initialization is used to call the initialization code from the constructor:
using MongoDBApp.Models;
using MongoDBApp.Services;
using MongoDBApp.Utility;
using PropertyChanged;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MongoDBApp.Extensions;
using System.Windows.Input;
using MongoDBApp.Common;
using MongoDBApp.Messages;
namespace MongoDBApp.ViewModels
{
[ImplementPropertyChanged]
public class CustomerOrdersViewModel : IPageViewModel, INotifyPropertyChanged
{
private IDataService<OrderModel> _orderDataService;
public CustomerOrdersViewModel(IDataService<OrderModel> orderDataService)
{
_customerOrders = new List<OrderModel>();
//{
// new OrderModel(){Email = "bvarley#gmail.com", Status = true}
//};
this._orderDataService = orderDataService;
this._dialogService = dialogservice;
Messenger.Default.Register<ProductModel>(this, OnUpdateProductMessageReceived);
this.Initialization = InitializeAsync();
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
#region properties
public string SelectedCustomerEmail { get; set; }
private IEnumerable<OrderModel> _customerOrders;
public IEnumerable<OrderModel> CustomerOrders
{
get { return this._customerOrders;}
set
{
_customerOrders = value;
OnPropertyChanged("CustomerOrders");
}
}
public OrderModel SelectedOrder { get; set; }
public Task Initialization { get; set; }
#endregion
#region methods
private async Task InitializeAsync()
{
var customer = await AwaitableMessages.NextMessageAsync<CustomerModel>();
SelectedCustomerEmail = customer.Email;
await LoadCustomerOrdersAsync(SelectedCustomerEmail);
}
public async Task LoadCustomerOrdersAsync(string email)
{
var ordersResult = await _orderDataService.GetAllByEmailAsync(email);
CustomerOrders = ordersResult.ToObservableCollection();
}
#endregion
}
}
This is also the associated view showing the binding setup:
<UserControl x:Class="MongoDBApp.Views.CustomerOrdersView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:boolean_converter="clr-namespace:MongoDBApp.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
d:DesignHeight="300"
d:DesignWidth="300"
mc:Ignorable="d">
<UserControl.Resources>
<boolean_converter:BooleanConverter x:Key="BooleanConverter" />
</UserControl.Resources>
<Viewbox>
<xctk:BusyIndicator IsBusy="{Binding ButtonEnabled}">
<Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="2*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding WindowLoadedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<DataGrid x:Name="customersgrid"
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="1"
Grid.ColumnSpan="4"
AutoGenerateColumns="False"
ItemsSource="{Binding CustomerOrders}"
SelectedItem="{Binding SelectedOrder}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Email}" Header="Email" />
<DataGridTextColumn Binding="{Binding Date}" Header="Date" />
<DataGridTextColumn Binding="{Binding Status}" Header="Shipping Status" />
</DataGrid.Columns>
</DataGrid>
<Label Grid.Row="4"
Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Content="Date:" />
<TextBlock Grid.Row="4"
Grid.Column="2"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Text="{Binding SelectedOrder.Date}"
TextWrapping="Wrap" />
<Label Grid.Row="4"
Grid.Column="3"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Content="Products:" />
<ComboBox Grid.Row="4"
Grid.Column="4"
Grid.ColumnSpan="4"
Width="120"
HorizontalAlignment="Left"
VerticalAlignment="Top"
DisplayMemberPath="ProductId"
ItemsSource="{Binding SelectedOrder.Products}"
ScrollViewer.VerticalScrollBarVisibility="Visible"
SelectedItem="{Binding SelectedProduct}" />
<Label Grid.Row="5"
Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Content="Email:" />
<TextBlock Grid.Row="5"
Grid.Column="2"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Text="{Binding SelectedOrder.Email}"
TextWrapping="Wrap" />
<RadioButton Grid.Row="5"
Grid.Column="3"
Grid.ColumnSpan="2"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Content="Shipped"
IsChecked="{Binding SelectedOrder.Status,
Converter={StaticResource BooleanConverter},
ConverterParameter='true',
Mode=TwoWay}" />
<RadioButton Grid.Row="5"
Grid.Column="4"
Grid.ColumnSpan="2"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Content="Processing"
IsChecked="{Binding SelectedOrder.Status,
Converter={StaticResource BooleanConverter},
ConverterParameter='false',
Mode=TwoWay}" />
</Grid>
</Grid>
</xctk:BusyIndicator>
</Viewbox>
There was nothing visibly wrong with your code; In my attempt to replicate the issue, I managed to do it without any problem. I am publishing my code here, Hope that will help.
My OrderModel class;
public class OrderModel
{
public string Email { get; set; }
public DateTime Date { get; set; }
public string Status { get; set; }
}
My ViewModel (I have used a BaseViewModel; that's optional. The way you have done is ok)
public class MainWindowViewModel:BaseViewModel
{
public MainWindowViewModel()
{
_customerOrders = new List<OrderModel>();
_customerOrders.Add(new OrderModel(){Date = DateTime.Now, Email = "mymail#gmail.com", Status = "Active"});
InitializeAsync();
}
private List<OrderModel> _customerOrders;
private OrderModel _selectedOrder;
public List<OrderModel> CustomerOrders
{
get { return this._customerOrders; }
set
{
_customerOrders = value;
OnPropertyChanged("CustomerOrders");
}
}
public OrderModel SelectedOrder
{
get { return _selectedOrder; }
set
{
_selectedOrder = value;
OnPropertyChanged("SelectedOrder");
}
}
private async void InitializeAsync()
{
CustomerOrders = await LoadCustomerOrdersAsync();
}
private async Task<List<OrderModel>> LoadCustomerOrdersAsync()
{
return await Task.Run(() => new List<OrderModel>()
{
new OrderModel() {Date = DateTime.Now, Email = "mymail1#gmail.com", Status = "Active"},
new OrderModel() {Date = DateTime.Now, Email = "mymail2#gmail.com", Status = "Active"},
new OrderModel() {Date = DateTime.Now, Email = "mymail3#gmail.com", Status = "Active"},
new OrderModel() {Date = DateTime.Now, Email = "mymail4#gmail.com", Status = "Active"},
new OrderModel() {Date = DateTime.Now, Email = "mymail5#gmail.com", Status = "Active"},
new OrderModel() {Date = DateTime.Now, Email = "mymail6#gmail.com", Status = "Active"},
});
}
}
My View
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DataGrid HorizontalAlignment="Left" Margin="31,33,0,0" VerticalAlignment="Top" Height="250" Width="455" ItemsSource="{Binding Path=CustomerOrders, Mode=TwoWay}" SelectedItem="{Binding Path=SelectedOrder, Mode=TwoWay}">
<!--<DataGrid.Columns>
<DataGridCheckBoxColumn Binding="{x:Null}" ClipboardContentBinding="{x:Null}" Header="Email" HeaderStringFormat="Email"/>
<DataGridCheckBoxColumn Binding="{x:Null}" ClipboardContentBinding="{x:Null}" Header="Date" HeaderStringFormat="Date"/>
<DataGridCheckBoxColumn Binding="{x:Null}" ClipboardContentBinding="{x:Null}" Header="Status" HeaderStringFormat="Status"/>
</DataGrid.Columns>-->
</DataGrid>
</Grid>
Code Behind;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
}
I came to a solution of placing the async call in the Window loaded Task, instead of the constructor.
Solution: (message handler first registers the passed over value, then the async load method is called from the window loaded Task)
public CustomerOrdersViewModel(IDataService<OrderModel> orderDataService, IDialogService dialogservice)
{
this._orderDataService = orderDataService;
this._dialogService = dialogservice;
Messenger.Default.Register<CustomerModel>(this, OnUpdateOrderMessageReceived);
LoadCommands();
}
private void OnUpdateOrderMessageReceived(CustomerModel customer)
{
SelectedCustomerEmail = customer.Email;
IsEnabled = true;
}
private async Task WindowLoadedAsync(object obj)
{
await LoadCustomerOrdersAsync(SelectedCustomerEmail);
}
I'm just having a play with WPF, and I'm having a problem with databinding...
Here's my code so far...
The Window XAML:
<Window x:Class="FRC.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Registry Cleaner - By Martin Milan." MinHeight ="350" Height ="350" MinWidth="525" MaxHeight="700" Width="350" Background="#FFC199AA" >
<DockPanel Background="#FFD9E1E8" Margin="10">
<Grid DockPanel.Dock="Top" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Label Grid.Column="0" Grid.Row="0" HorizontalAlignment="Right" >Filepath:</Label>
<TextBox Grid.Column="1" HorizontalAlignment="Stretch" Name="txtFilePath" VerticalAlignment="Stretch" />
</Grid>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" >
<Button Name="butScan" Content="Scan" MinWidth="75" Margin="0,0,10,5" />
<Button Name="butDelete" Content="Remove RegKeys" Margin="0,0,5,5" Click="butDelete_Click" />
</StackPanel>
<ScrollViewer Margin="0,0,0,5">
<DataGrid AutoGenerateColumns="False" Name="dgActions" CanUserAddRows="False" CanUserDeleteRows="False" >
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding RegKeyPath, Mode=OneWay}" Header="Registry Key" Width="*"/>
<DataGridCheckBoxColumn Binding="{Binding DeletePath, Mode=TwoWay}" Header="Can I delete key?"
MinWidth="110" Width="110" />
</DataGrid.Columns>
</DataGrid>
</ScrollViewer>
</DockPanel>
</Window>
The code behind for the window:
namespace FRC
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
protected List<RegistryAction> mRegistryActions = new List<RegistryAction>();
public MainWindow()
{
InitializeComponent();
RegistryAction oRegAction = new RegistryAction();
oRegAction.DeletePath = true;
oRegAction.RegKeyPath = "A test value";
mRegistryActions.Add (oRegAction);
dgActions.DataContext = mRegistryActions;
dgActions.ItemsSource = mRegistryActions;
}
private void butDelete_Click(object sender, RoutedEventArgs e)
{
RegistryAction oRegAction = new RegistryAction();
oRegAction.DeletePath = true;
Random rGen = new Random();
oRegAction.RegKeyPath = "A test " + rGen.Next(100).ToString();
mRegistryActions.Add(oRegAction);
}
}
}
The RegistryAction class:
namespace FRC
{
public class RegistryAction
{
public string RegKeyPath { get; set; }
public bool DeletePath { get; set; }
public RegistryAction()
{
this.DeletePath = false;
this.RegKeyPath = "";
}
}
}
Basically. it sets up a list of RegistryAction objects, and binds it with a DataGrid. I am finding however that whenever I run the code in butDelete_Click, although the list is updated, the content isn't getting updated on the Grid.
In short, can anyone spot what I have missed please?
Martin.
mRegistryActions should be ObservableCollection:
protected ObservableCollection<RegistryAction> mRegistryActions = new ObservableCollection<RegistryAction>();
You need to have RegistryAction implement INotifyPropertyChanged. MSDN also has a how-to on this subject.