I've come across the situation, where I need access to the controls in the View from the ViewModel. In order to code a method that adds the selected item from a ComboBox to a list.
My question is how do I access the View controls from the ViewModel? Is there a certain design pattern I should follow to allow this?
The method below is coded in the View's code behind, which I know is bad practice if following the MVVM pattern, due to the tight coupling involved. Which is why I'm aiming to move this method the the ViewModel.
The method's purpose is to take the currently selected items from two ComboBoxes and add to a Key/Value List:
public void AddGradeSubjectChoiceToList()
{
string SelectedSubjectName = "null data";
int SelectedPoints = 01;
SelectedSubjectName = subjectCmbBx.SelectedItem.ToString();
try {
SelectedPoints = int.Parse(ordinaryGradeCmbBx.SelectedValue.ToString());
}
catch (Exception e)
{
//log error here..
}
List<StringKeyValue> SubjectPointKVTemp = new List<StringKeyValue>();
//Add selected pair to list
SubjectPointKVTemp.Add(new StringKeyValue { Key = SelectedSubjectName, Value = SelectedPoints });
SubjectPointKV = SubjectPointKVTemp;
}
The XAML for the MainPage is set up like this, with two combo boxes, for subjects and grades. And the addGrade button which will call the method to add the selected pair to a list:
<Page x:Class="LC_Points.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Core="using:Microsoft.Xaml.Interactions.Core"
xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:converter="using:LC_Points.Converter"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:local="using:LC_Points"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
DataContext="{Binding Source={StaticResource Locator}}"
mc:Ignorable="d">
<Page.Resources>
<converter:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converter:BoolToNonVisibilityConverter x:Key="BoolToNonVisibilityConverter" />
</Page.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40*" />
<RowDefinition Height="20*" />
<RowDefinition Height="30*" />
<RowDefinition Height="30*" />
<RowDefinition Height="20*" />
<RowDefinition Height="20*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="4*" />
<ColumnDefinition Width="3*" />
</Grid.ColumnDefinitions>
<ComboBox x:Name="subjectCmbBx"
Grid.Row="1"
Grid.ColumnSpan="2"
Width="174"
HorizontalAlignment="Left"
VerticalAlignment="Top"
DisplayMemberPath="Subject"
Header="Subjects"
ItemsSource="{Binding Subjects}"
PlaceholderText="Pick a subject" />
<ComboBox x:Name="ordinaryGradeCmbBx"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Width="170"
HorizontalAlignment="Right"
DisplayMemberPath="Key"
Header="Grades"
ItemsSource="{Binding OrdinaryGradePointKV}"
PlaceholderText="Pick a grade"
Visibility="{Binding IsHigherToggled,
Mode=TwoWay,
Converter={StaticResource BoolToNonVisibilityConverter}}" />
<Button x:Name="addGradeBtn"
Grid.Row="2"
HorizontalAlignment="Left"
Command="{Binding Path=AddGradeCommand}"
Content="Add Grade" />
<ToggleButton x:Name="ordinaryTglBtn"
Grid.Row="3"
Grid.ColumnSpan="2"
HorizontalAlignment="Center"
Content="Ordinary"
IsChecked="{Binding IsOrdinaryToggled,
Mode=TwoWay}" />
</Grid>
</Page>
The stripped down implementation of the ViewModel is as follows for reference, showing in comments how I pan to implement the AddGradeSubjectChoiceToList() method:
namespace LC_Points.ViewModel
{
public class MainViewModel : ViewModelBase
{
private ScoreModel _scoreModel;
/// <summary>
/// Initializes a new instance of the MainViewModel class.
/// </summary>
public MainViewModel(ScoreModel GradeModel)
{
_scoreModel = GradeModel;
//call methods to initilise list data
GetSubjectTypes();
GetOrdinaryGradePairs();
}
public List<ScoreModel> Subjects { get; set; }
public List<StringKeyValue> OrdinaryGradePointKV { get; set; }
//ordinary toggle button bool
private bool _isOrdinaryToggled;
public bool IsOrdinaryToggled
{
get
{
return _isOrdinaryToggled;
}
set
{
_isOrdinaryToggled = value;
RaisePropertyChanged("IsOrdinaryToggled");
}
}
//Need to add same method from code behind to VM here
//but don't have access to the View's controlsMethod to store Subject and Grade from Combo Boxes
public void AddGradeSubjectChoiceToList()
{
}
//This Relay Command is tied to the button in the View, that will be used
//to call the AddGradeSubjectChoiceToList method
RelayCommand addGradeCommand;
public RelayCommand AddGradeCommand
{
get
{
if (addGradeCommand == null)
{
addGradeCommand = new RelayCommand(() =>
{
AddGradeSubjectChoiceToList
});
}
return addGradeCommand;
}
}
public class StringKeyValue
{
public string Key { get; set; }
public int Value { get; set; }
}
public void GetOrdinaryGradePairs()
{
List<StringKeyValue> ordinaryGradePointKVTemp = new List<StringKeyValue>();
ordinaryGradePointKVTemp.Add(new StringKeyValue { Key = "A1", Value = 60 });
ordinaryGradePointKVTemp.Add(new StringKeyValue { Key = "A2", Value = 50 });
ordinaryGradePointKVTemp.Add(new StringKeyValue { Key = "B1", Value = 45 });
ordinaryGradePointKVTemp.Add(new StringKeyValue { Key = "B2", Value = 40 });
ordinaryGradePointKVTemp.Add(new StringKeyValue { Key = "B3", Value = 35 });
ordinaryGradePointKVTemp.Add(new StringKeyValue { Key = "C1", Value = 30 });
ordinaryGradePointKVTemp.Add(new StringKeyValue { Key = "C2", Value = 25 });
ordinaryGradePointKVTemp.Add(new StringKeyValue { Key = "C3", Value = 20 });
OrdinaryGradePointKV = ordinaryGradePointKVTemp;
}
public void GetSubjectTypes()
{
List<ScoreModel> subjectList = new List<ScoreModel>();
// Adding Subjects to List
subjectList.Add(new ScoreModel { Subject = "Accounting" });
subjectList.Add(new ScoreModel { Subject = "Agricultural Economics" });
subjectList.Add(new ScoreModel { Subject = "Agricultural Science" });
subjectList.Add(new ScoreModel { Subject = "Ancient Greek" });
subjectList.Add(new ScoreModel { Subject = "Applied Math" });
subjectList.Add(new ScoreModel { Subject = "Arabic" });
subjectList.Add(new ScoreModel { Subject = "Art" });
subjectList.Add(new ScoreModel { Subject = "Artistic & Creative Group" });
subjectList.Add(new ScoreModel { Subject = "Biology" });
subjectList.Add(new ScoreModel { Subject = "Business" });
Subjects = subjectList;
}
}
}
In order to follow MVVM you should use generic types and bind them to the control.
Here's an example in one of my ViewModels:
private string[] _optionItems;
public string[] OptionItems
{
get
{
return _optionItems;
}
set
{
if (_optionItems == value)
return;
_optionItems = value;
OnPropertyChanged();
}
}
private string _selectedOption;
public string SelectedOption
{
get
{
return _selectedOption;
}
set
{
if (_selectedOption == value)
return;
_selectedOption = value;
OnPropertyChanged();
}
}
and here's the XAML code:
<ComboBox ItemsSource="{Binding OptionItems}" SelectedItem="{Binding SelectedOption}"/>
As the above comments point out, the solution of coupling the View to the ViewModel is bad practice when following the MVVM pattern.
The appropriate solution is to set up a property for the ComboBox's SelectedItem and bind the View to that, so that the Selected items are available for leveraging in the VM.
1. Set up binding to SelectedItem in the View:
SelectedItem="{Binding SelectedSubject,Mode=TwoWay}"
2. Create the ComboBox property in the ViewModel:
private ComboBox _selectedSubject;
public ComboBox SelectedSubject
{
get { return _selectedSubject; }
set
{
_selectedSubject = value;
RaisePropertyChanged("SelectedSubject");
}
}
Related
I have a ComboBox that allows the user to select a category and a ListView that is bound to an ObservableCollection of items in the selected category. When the user selects a different category, the items in the collection are updated. Sometimes this works as expected, but sometimes the list of items is mangled. It shows a duplicate item when there should be two separate items.
The results seem to depend on which category I'm switching from. For example, if I switch from a category with no items to a category with two items, the same item is shown twice. But if I switch from a category with four items to that same category with two items, they are shown correctly.
Here is a repro:
MainPage.xaml
<Page
x:Class="ListViewDuplicateItem_Binding.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ListViewDuplicateItem_Binding">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<ComboBox
Grid.Row="0"
Grid.Column="0"
ItemsSource="{Binding ViewModel.Groups}"
SelectedItem="{Binding ViewModel.SelectedGroup, Mode=TwoWay}" />
<ListView
Grid.Row="1"
Grid.Column="0"
ItemsSource="{Binding ViewModel.Widgets}"
SelectedItem="{Binding ViewModel.SelectedWidget, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:Widget">
<TextBlock Text="{Binding Id}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<local:MyControl
Grid.Row="1"
Grid.Column="1"
Text="{Binding ViewModel.SelectedWidget.Id, Mode=OneWay}" />
</Grid>
</Page>
MainPage.xaml.cs
using Windows.UI.Xaml.Controls;
namespace ListViewDuplicateItem_Binding
{
public sealed partial class MainPage : Page
{
public MainPage()
{
InitializeComponent();
DataContext = this;
}
public MainViewModel ViewModel { get; } = new MainViewModel();
}
}
MainViewModel.cs
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
namespace ListViewDuplicateItem_Binding
{
public class MainViewModel : INotifyPropertyChanged
{
private string _selectedGroup;
private Widget _selectedWidget;
public MainViewModel()
{
PropertyChanged += HomeViewModel_PropertyChanged;
SelectedGroup = Groups.First();
}
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<string> Groups { get; } = new ObservableCollection<string>(DataSource.AllGroups);
public string SelectedGroup
{
get => _selectedGroup;
set
{
if (_selectedGroup != value)
{
_selectedGroup = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedGroup)));
}
}
}
public Widget SelectedWidget
{
get => _selectedWidget;
set
{
if (_selectedWidget != value)
{
_selectedWidget = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedWidget)));
}
}
}
public ObservableCollection<Widget> Widgets { get; } = new ObservableCollection<Widget>();
private void HomeViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SelectedGroup))
{
var widgetsToLoad = DataSource.GetWidgetsForGroup(SelectedGroup);
// Add widgets in this group
widgetsToLoad.Except(Widgets).ToList().ForEach(w => Widgets.Add(w));
// Remove widgets not in this group
Widgets.Except(widgetsToLoad).ToList().ForEach(w => Widgets.Remove(w));
// Select the first widget
if (SelectedWidget == null && Widgets.Any())
{
SelectedWidget = Widgets.First();
}
}
}
}
}
DataSource.cs
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace ListViewDuplicateItem_Binding
{
public static class DataSource
{
public static ObservableCollection<string> AllGroups { get; } = new ObservableCollection<string>
{
"First Widget",
"First Two Widgets",
"Last Two Widgets",
"All Widgets",
"None"
};
public static List<Widget> AllWidgets { get; } = new List<Widget>
{
new Widget()
{
Id = 1,
},
new Widget()
{
Id = 2,
},
new Widget()
{
Id = 3,
},
new Widget()
{
Id = 4,
}
};
public static List<Widget> GetWidgetsForGroup(string group)
{
switch (group)
{
case "First Widget":
return new List<Widget> { AllWidgets[0] };
case "First Two Widgets":
return new List<Widget> { AllWidgets[0], AllWidgets[1] };
case "Last Two Widgets":
return new List<Widget> { AllWidgets[2], AllWidgets[3] };
case "All Widgets":
return new List<Widget>(AllWidgets);
default:
return new List<Widget>();
}
}
}
}
Widget.cs
namespace ListViewDuplicateItem_Binding
{
public class Widget
{
public int Id { get; set; }
}
}
MyControl.xaml
<UserControl
x:Class="ListViewDuplicateItem_Binding.MyControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<TextBox Text="{x:Bind Text, Mode=TwoWay}" />
</UserControl>
MyControl.xaml.cs
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace ListViewDuplicateItem_Binding
{
public sealed partial class MyControl : UserControl
{
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(MyControl), new PropertyMetadata(null));
public MyControl()
{
InitializeComponent();
}
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
}
}
This only seems to occur when the project includes a custom control that uses the Binding markup.
In the example above, if MyControl is removed from MainPage.xaml, it works as expected.
Likewise, if <local:MyControl Text="{Binding ViewModel.SelectedWidget.Id}" /> is changed to <local:MyControl Text="{x:Bind ViewModel.SelectedWidget.Id}" />, the example works as expected
This appears to be a bug in the ListView control, but you can work around it by using {x:Bind} compiled bindings.
Edit: Upon further investigation, the custom control may have been a red herring. Changing the custom control to a standard TextBox does not resolve the issue as I previously thought. The issue can be reproduced without the custom control. Nonetheless, using {x:Bind} or removing the control entirely does resolve the issue in this case.
Updating my project to use {x:Bind} (compiled bindings) appeared to resolve the issue, but a week later I unexpectedly started seeing duplicate items in my ListView again. This time I discovered three other factors that contributed to this issue.
I added a FallbackValue to the TextBoxes bound to the SelectedItem so they would be cleared when no item was selected. If I remove the FallbackValue, the list items are not duplicated. However, I need this setting.
I discovered that the order in which I add and remove items with the ObservableCollection bound to the ListView is important. If I add new items first and then remove old items, list items are duplicated. If I remove old items first and then add new items, the items are not duplicated. However, I'm using AutoMapper.Collection to update this collection, so I have no control over the order.
A colleague suggested that this bug may be related to the ListView.SelectedItem. I discovered that if I set the selected item to null before removing it from the collection, list items are not duplicated. This is the solution I am now using.
Here's an example:
// This resolves the issue:
if (!widgetsToLoad.Contains(SelectedWidget))
{
SelectedWidget = null;
}
// AutoMapper.Collection updates collections in this order. The issue does not occur
// if the order of these two lines of code is reversed.
{
// Add widgets in this group
widgetsToLoad.Except(Widgets).ToList().ForEach(w => Widgets.Add(w));
// Remove widgets not in this group
Widgets.Except(widgetsToLoad).ToList().ForEach(w => Widgets.Remove(w));
}
For a full repro, replace the code blocks in the question with these changes:
MainPage.xaml
<Page
x:Class="ListViewDuplicateItem_Fallback.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ListViewDuplicateItem_Fallback">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<ComboBox
Grid.Row="0"
Grid.Column="0"
ItemsSource="{x:Bind ViewModel.Groups}"
SelectedItem="{x:Bind ViewModel.SelectedGroup, Mode=TwoWay}" />
<ListView
Grid.Row="1"
Grid.Column="0"
ItemsSource="{x:Bind ViewModel.Widgets}"
SelectedItem="{x:Bind ViewModel.SelectedWidget, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:Widget">
<TextBlock Text="{x:Bind Id}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<TextBox
Grid.Row="1"
Grid.Column="1"
Text="{x:Bind ViewModel.SelectedWidget.Id, Mode=OneWay, FallbackValue=''}" />
</Grid>
</Page>
MainViewModel.cs
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
namespace ListViewDuplicateItem_Fallback
{
public class MainViewModel : INotifyPropertyChanged
{
private string _selectedGroup;
private Widget _selectedWidget;
public MainViewModel()
{
PropertyChanged += HomeViewModel_PropertyChanged;
SelectedGroup = Groups.First();
}
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<string> Groups { get; } = new ObservableCollection<string>(DataSource.AllGroups);
public string SelectedGroup
{
get => _selectedGroup;
set
{
if (_selectedGroup != value)
{
_selectedGroup = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedGroup)));
}
}
}
public Widget SelectedWidget
{
get => _selectedWidget;
set
{
if (_selectedWidget != value)
{
_selectedWidget = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedWidget)));
}
}
}
public ObservableCollection<Widget> Widgets { get; } = new ObservableCollection<Widget>();
private void HomeViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SelectedGroup))
{
var widgetsToLoad = DataSource.GetWidgetsForGroup(SelectedGroup);
// This resolves the issue:
//if (!widgetsToLoad.Contains(SelectedWidget))
//{
// SelectedWidget = null;
//}
// AutoMapper.Collection updates collections in this order. The issue does not occur
// if the order of these two lines of code is reversed. I do not simply clear the
// collection and reload it because this clears the selected item even when it is in
// both groups, and the animation is much smoother if items are not removed and reloaded.
{
// Add widgets in this group
widgetsToLoad.Except(Widgets).ToList().ForEach(w => Widgets.Add(w));
// Remove widgets not in this group
Widgets.Except(widgetsToLoad).ToList().ForEach(w => Widgets.Remove(w));
}
// Select the first widget
if (SelectedWidget == null && Widgets.Any())
{
SelectedWidget = Widgets.First();
}
}
}
}
}
DataSource.cs
using System.Collections.Generic;
using System.Linq;
namespace ListViewDuplicateItem_Fallback
{
public static class DataSource
{
public static List<string> AllGroups { get; set; } = new List<string> { "Group 1", "Group 2", "Group 3" };
public static List<Widget> AllWidgets { get; set; } = new List<Widget>(Enumerable.Range(1, 11).Select(widgetId => new Widget { Id = widgetId }));
public static List<Widget> GetWidgetsForGroup(string group)
{
switch (group)
{
case "Group 1":
return AllWidgets.Take(4).ToList();
case "Group 2":
return AllWidgets.Skip(4).Take(4).ToList();
case "Group 3":
return AllWidgets.Take(1).Union(AllWidgets.Skip(8).Take(3)).ToList();
default:
return new List<Widget>();
}
}
}
}
I have an WPF application using MVVM.
I have a DataGrid bound to an ObservableCollection and a TextBox bound to the DataGrid SelectedItem, so when I click an item in the DataGrid, the TextBox is populated.
I also have a Button using Command and CommandParameter and using RelayCommand to check if the TextBox is empty and then disabling the Button.
That all works great, if I use UpdateSourceTrigger=PropertyChanged. The thing I don't like is because of the binding, if the user changes the text in the TextBox, the DataGrid record is edited. If the user then changes their mind about changing the record, and clicks somewhere else, the record in the DataGrid still shows the edited text.
What I have tried is using Mode=OneWay on the TextBox binding, which works in that it doesn't update the DataGrid record. After the data is saved to the database, I need to manually refresh the DataGrid to show the changes.
The code I have in my code behind is the DataGrid's SelectionChanged event which sets a property on the ViewModel to the selected item.
So in order to show the new changes, I thought adding a call to my GetCategories again after the changes would work. However when the code executes OnPropertyChanged("ReceivedCategories"), my CurrentCategory property becomes null.
My code:
CategoryModel.cs
public class CategoryModel
{
public int CategoryID { get; set; }
public string Description { get; set; }
readonly SalesLinkerDataContext _dbContext = new SalesLinkerDataContext();
public ObservableCollection<CategoryModel> GetCategories()
{
var result = _dbContext.tblSalesCategories.ToList();
List<CategoryModel> categoriesList = result.Select(item => new CategoryModel
{
CategoryID = item.CategoryID,
Description = item.Description.Trim()
}).ToList();
return new ObservableCollection<CategoryModel>(categoriesList);
}
internal bool UpdateCategory(int id, string description)
{
if (_dbContext.tblSalesCategories.Any(x => x.Description == description))
{
MessageBox.Show("A category with the same name already exists.");
return false;
}
try
{
var category = (from a in _dbContext.tblSalesCategories
where a.CategoryID == id
select a).FirstOrDefault();
if (category != null)
{
category.Description = description;
_dbContext.SubmitChanges();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
return false;
}
return true;
}
internal bool AddCategory(string description)
{
if (_dbContext.tblSalesCategories.Any(x => x.Description == description))
{
MessageBox.Show("A category with the same name already exists.");
return false;
}
var newCategory = new tblSalesCategory();
newCategory.Description = description;
try
{
_dbContext.tblSalesCategories.InsertOnSubmit(newCategory);
_dbContext.SubmitChanges();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
return false;
}
return true;
}
internal bool DeleteCategory(int id)
{
var result = _dbContext.tblSalesCategories.FirstOrDefault(x => x.CategoryID == id);
try
{
if (result != null)
{
_dbContext.tblSalesCategories.DeleteOnSubmit(result);
_dbContext.SubmitChanges();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
return false;
}
return true;
}
}
CategoriesViewModel.cs
public class CategoriesViewModel : ViewModelBase, IPageViewModel
{
public CategoryModel CurrentCategory = new CategoryModel();
public ObservableCollection<CategoryModel> Categories = new ObservableCollection<CategoryModel>();
public RelayCommand GetCategoriesRelay;
public RelayCommand UpdateCategoryRelay;
public RelayCommand AddCategoryRelay;
public RelayCommand DeleteCategoryRelay;
#region Get Categories Command
public ICommand GetCategoriesCommand
{
get
{
GetCategoriesRelay = new RelayCommand(p => GetCategories(),
p => CanGetCategories());
return GetCategoriesRelay;
}
}
private bool CanGetCategories()
{
return true;
}
private void GetCategories()
{
Categories = CurrentCategory.GetCategories();
ReceivedCategories = Categories;
}
#endregion
#region Update Category Command
public ICommand UpdateCategoryCommand
{
get
{
UpdateCategoryRelay = new RelayCommand(p => UpdateCategory((string) p),
p => CanUpdateCategory());
return UpdateCategoryRelay;
}
}
public bool CanUpdateCategory()
{
return !String.IsNullOrWhiteSpace(Description);
}
public void UpdateCategory(string description)
{
if (CurrentCategory.UpdateCategory(CurrentCategory.CategoryID, description))
{
GetCategories();
}
}
#endregion
#region Add Category Command
public ICommand AddCategoryCommand
{
get
{
AddCategoryRelay = new RelayCommand(p => AddCategory((string) p),
p => CanAddCategory());
return AddCategoryRelay;
}
}
private bool CanAddCategory()
{
return !String.IsNullOrWhiteSpace(Description);
}
private void AddCategory(string description)
{
if (CurrentCategory.AddCategory(description))
GetCategories();
}
#endregion
#region Delete Category Command
public ICommand DeleteCategoryCommand
{
get
{
DeleteCategoryRelay = new RelayCommand(p => DeleteCategory((int) p),
p => CanDeleteCategory());
return DeleteCategoryRelay;
}
}
private bool CanDeleteCategory()
{
return true;
}
private void DeleteCategory(int id)
{
if (CurrentCategory.DeleteCategory(id))
GetCategories();
}
#endregion
/// <summary>
/// Describes the name that will be used for the menu option
/// </summary>
public string Name
{
get { return "Manage Categories"; }
}
public string Description
{
get
{
return CurrentCategory.Description;
}
set
{
CurrentCategory.Description = value;
OnPropertyChanged("Description");
}
}
public ObservableCollection<CategoryModel> ReceivedCategories
{
get { return Categories; }
set
{
Categories = value;
OnPropertyChanged("ReceivedCategories");
}
}
}
CategoryView.xaml
<UserControl x:Class="SalesLinker.CategoriesView"
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:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="600" Background="White">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding GetCategoriesCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="45"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Margin="20,0,0,0" FontSize="20" HorizontalAlignment="Center" Content="Categories"/>
<DataGrid x:Name="LstCategories" Grid.Column="0" Grid.Row="1" AutoGenerateColumns="false"
ItemsSource="{Binding Path=ReceivedCategories, Mode=TwoWay}" SelectionChanged="Selector_OnSelectionChanged"
HorizontalScrollBarVisibility="Disabled" GridLinesVisibility="None"
CanUserAddRows="False" CanUserDeleteRows="False" CanUserSortColumns="True" Background="White">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=Description}" IsReadOnly="True" Header="Description" Width="300" />
</DataGrid.Columns>
</DataGrid>
<Button Command="{Binding AddCategoryCommand}" Grid.Column="1" Grid.Row="1" VerticalAlignment="Top" Height="50" Width="50" Margin="0,20,0,0" Background="Transparent" BorderThickness="0" BorderBrush="Transparent"
CommandParameter="{Binding ElementName=TbDescription, Path=Text}">
<Image Source="/Images/Plus.png"/>
</Button>
<Button Command="{Binding DeleteCategoryCommand}" Grid.Column="1" Grid.Row="1" VerticalAlignment="Top" Height="50" Width="50" Margin="0,75,0,0" Background="Transparent" BorderThickness="0" BorderBrush="Transparent"
CommandParameter="{Binding SelectedItem.CategoryID, ElementName=LstCategories, Mode=OneWay }">
<Image Source="/Images/Minus.png"/>
</Button>
<Grid Grid.Row="1" Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="50"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="75"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label VerticalAlignment="Center" HorizontalAlignment="Center" Grid.Row="0" Grid.Column="0" Content="Description:"/>
<TextBox x:Name="TbDescription" DataContext="CategoryModel" Grid.Row="0"
Grid.Column="1" Width="250" Height="Auto" VerticalAlignment="Center"
HorizontalAlignment="Left" Margin="10,0,0,0"
Text="{Binding SelectedItem.Description, ElementName=LstCategories, Mode=OneWay}"/>
<Button Grid.Row="1" Grid.Column="1" HorizontalAlignment="Left" Margin="10,0,0,0"
Height="20" Width="120" Content="Update Description"
Command="{Binding UpdateCategoryCommand}"
CommandParameter="{Binding ElementName=TbDescription, Path=Text}" />
</Grid>
</Grid>
And I have also just noticed that using Mode=OneWay on the TextBox breaks my CanExecute pieces of code as well.
So all I can think of is either:
Find a way to bind another property to the TextBox as well?
Find a way of using UpdateSourceTrigger=PropertyChanged on the TextBox, but prevent the DataGrid being updated.
Any ideas?
For the null value in the Observable List you have to update the list with a copy of it and an Dispatcher.Invoke otherwise the gui will crash or show null I had a similar problem on update a observable list in a thread.
So you should write your changes in a list like:
//Your Copy List
ObservableCollection<CategoryModel> _ReceivedCategories;
//Your Execute command for the Gui
public void onUpdateExecuted(object parameter){
Dispatcher.Invoke(new Action(() => ReceivedCategories = new ObservableCollection <CategoryModel> (_ReceivedCategories));
}
//Your Undo Command if the Inputs are not ok
public void onUndoExecuted(object parameter){
Dispatcher.Invoke(new Action(() => _ReceivedCategories = new ObservableCollection <CategoryModel> (ReceivedCategories));
}
//Your Command on every input which is set
public void onInputExecuted(object parameter){
_ReceivedCategories.add(Your Object);
}
And you can add an update Command for the Main List so you first update the Copy List with the values and after an Command set the Copy Collection in the Main Collection
Hope that helps
I couldn't get the above suggestion by SeeuD1 to work, but have have figured out what the problem was.
I have The DataGrid's ItemsSource bound to ReceivedCategories and in the code behind, the SelectionChanged event to use to bind to the TextBox.
After the database save, I call my GetCategories() again to refresh the data.
So when ReceivedCategories gets the data again, the DataGrid's ItemsSource is updated, but not before the SelectionChange event fires trying to set my CurrentCategory to the SelectedItem. However there is no longer anything selected in the DataGrid (index is -1), so assignment fails and sets CurrentCategory to null.
Fix is to simply only assign CurrentCategory to the SelectedItem if something is selected.
Old code:
private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var viewmodel = (CategoriesViewModel)DataContext;
viewmodel.CurrentCategory = LstCategories.SelectedItems.Cast<CategoryModel>().FirstOrDefault();
}
Fixed:
private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var viewmodel = (CategoriesViewModel)DataContext;
if (LstCategories.SelectedIndex > -1)
viewmodel.CurrentCategory = LstCategories.SelectedItems.Cast<CategoryModel>().FirstOrDefault();
}
Thanks for the suggestions.
I'm trying to execute methods based on listview items data. In addition to that, the button, which triggers the command, should only be enabled, if "CanExecute" method of the listview item returns true.
Both methods, "MyCommand" and "CanExecute", are included in my ViewModel.
Unfortunately I'm not sure how to pass the items information correctly to both methods in order to be conform with the PRISM 6 framework.
So my first approach was to do it like the following :
Model
public class MyModel
{
public string Name { get; set; }
public string Version { get; set; }
public int Identifier { get; set; }
}
ViewModel
public class MyViewModel : BindableBase
{
private ObservableCollection<MyModel> _models = new ObservableCollection<MyModel>();
public ObservableCollection<MyModel> Models
{
get { return _models; }
set { SetProperty(ref _models, value); }
}
public DelegateCommand VerifyCommand { get; set; }
public MyViewModel()
{
//Add test data
for (int i = 0; i < 5; i++)
{
MyModel model = new MyModel();
model.Name = "Random Text";
model.Version = "Random Text";
model.Identifier = i;
Models.Add(model);
}
//Doesn't work, because I don't reference to "Models"
//How to do that?
VerifyCommand = new DelegateCommand(DoCommand, CanExecute).ObservesProperty<string>(() => Name).ObservesProperty<string>(() => Version);
}
private bool CanExecute()
{
//Obviously this doesn't work, because "Version" and "Name"
//don't belong to the selected "Models" item of the listview
//What is the "bridge", to know which item of Models was clicked (button)
return !String.IsNullOrWhiteSpace(Version) && !String.IsNullOrWhiteSpace(Name);
}
private void DoCommand()
{
//Do something special
}
}
View
<ListView ItemsSource="{Binding Models}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Height="Auto" Margin="0,0,0,10">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBox Grid.Row="0" Tag="VERSION" Text="{Binding Version, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Grid.Row="1" Tag="NAME" Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" />
<Button Command="{Binding ElementName=root, Path=DataContext.VerifyCommand}" Content="Verify" Grid.Row="2">
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The link between View and ViewModel is done by using:
prism:ViewModelLocator.AutoWireViewModel="True"
in my View (this works).
So in summary:
How does it work, PRISM conform, to 1. Enable the items button only if CanExecute is true and 2. to execute "DoCommand" method and passing items information to that (root element of the button -> In this case the ListViewItem (MyModel).
Any help would be greatly appreciated.
Short answer: put the command in the item's viewmodel.
Long answer:
Here's an example of what I mean in the comment above. I've omitted the observability of the collections, if you really need an observable collection of models and an observable collection of view models, prepare yourself for a lot of boring two-way-sync-code...
Model:
internal class ItemModel
{
public string Name { get; set; }
public string Version { get; set; }
public int Identifier { get; set; }
}
ViewModels (one for the collection of items, that is, your MyViewModel, and one for the item):
internal class MyCollectionViewModel : BindableBase
{
private readonly List<ItemModel> _models = new List<ItemModel>();
public MyCollectionViewModel()
{
//Add test data
for (var i = 0; i < 5; i++)
_models.Add( new ItemModel
{
// to prove that CanExecute is actually evaluated...
Name = i == 3 ? "Random Text" : string.Empty,
Version = "Random Text",
Identifier = i
} );
}
public IReadOnlyCollection<ItemViewModel> TheCollection => _models.Select( x => new ItemViewModel( x ) ).ToList();
}
internal class ItemViewModel : BindableBase
{
public ItemViewModel( ItemModel item )
{
_item = item;
VerifyCommand = new DelegateCommand( () =>
{
/* Do something */
}, () => !string.IsNullOrWhiteSpace( Version ) && !string.IsNullOrWhiteSpace( Name ) );
}
public string Name => _item.Name;
public string Version => _item.Version;
public int Identifier => _item.Identifier;
public DelegateCommand VerifyCommand
{
get;
}
private readonly ItemModel _item;
}
View:
<ListView ItemsSource="{Binding TheCollection}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Height="Auto" Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding Version, Mode=OneWay}" />
<TextBox Grid.Column="1" Text="{Binding Name, Mode=OneWay}" />
<Button Grid.Column="2" Command="{Binding VerifyCommand}" Content="Verify"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I've got a WPF Grid and would like to move rows up or down depending on the user's input. This is what I've tried so far (an example for when the user decides to move an element up):
RowDefinition currentRow = fieldsGrid.RowDefinitions[currentIndex];
fieldsGrid.RowDefinitions.Remove(currentRow);
fieldsGrid.RowDefinitions.Insert(currentIndex - 1, currentRow);
Am I doing something wrong? As the UI remains the same using this approach.
This would be the WPF approach to what you're screenshot looks like:
<Window x:Class="WpfApplication4.Window9"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window9" Height="300" Width="500">
<ItemsControl ItemsSource="{Binding Columns}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<DataTemplate.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisConverter"/>
</DataTemplate.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="50"/>
<ColumnDefinition/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="25"/>
<ColumnDefinition Width="25"/>
</Grid.ColumnDefinitions>
<!-- This is your Key image, I used a rectangle instead, you can change it -->
<Rectangle Fill="Yellow" Visibility="{Binding IsPrimaryKey, Converter={StaticResource BoolToVisConverter}}" Margin="2"/>
<CheckBox IsChecked="{Binding IsSelected}" Grid.Column="1"/>
<TextBlock Text="{Binding Name}" Grid.Column="2"/>
<ComboBox ItemsSource="{Binding SortOrders}" SelectedItem="{Binding SortOrder}" Grid.Column="3" Margin="2"/>
<Button Content="Up" Grid.Column="4" Margin="2"
Command="{Binding DataContext.MoveUpCommand, RelativeSource={RelativeSource FindAncestor, AncestorType=ItemsControl}}"
CommandParameter="{Binding}"/>
<Button Content="Down" Grid.Column="5" Margin="2"
Command="{Binding DataContext.MoveDownCommand, RelativeSource={RelativeSource FindAncestor, AncestorType=ItemsControl}}"
CommandParameter="{Binding}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
Code Behind:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using InduraClientCommon.MVVM;
using System.Collections.ObjectModel;
namespace WpfApplication4
{
public partial class Window9 : Window
{
public Window9()
{
InitializeComponent();
var vm = new ColumnListViewModel();
vm.Columns.Add(new ColumnViewModel() { IsPrimaryKey = true, Name = "Customer ID", SortOrder = SortOrder.Ascending });
vm.Columns.Add(new ColumnViewModel() {Name = "Customer Name", SortOrder = SortOrder.Descending});
vm.Columns.Add(new ColumnViewModel() {Name = "Customer Age", SortOrder = SortOrder.Unsorted});
DataContext = vm;
}
}
}
ViewModel:
public class ColumnListViewModel: ViewModelBase
{
private ObservableCollection<ColumnViewModel> _columns;
public ObservableCollection<ColumnViewModel> Columns
{
get { return _columns ?? (_columns = new ObservableCollection<ColumnViewModel>()); }
}
private DelegateCommand<ColumnViewModel> _moveUpCommand;
public DelegateCommand<ColumnViewModel> MoveUpCommand
{
get { return _moveUpCommand ?? (_moveUpCommand = new DelegateCommand<ColumnViewModel>(MoveUp, x => Columns.IndexOf(x) > 0)); }
}
private DelegateCommand<ColumnViewModel> _moveDownCommand;
public DelegateCommand<ColumnViewModel> MoveDownCommand
{
get { return _moveDownCommand ?? (_moveDownCommand = new DelegateCommand<ColumnViewModel>(MoveDown, x => Columns.IndexOf(x) < Columns.Count)); }
}
private void MoveUp(ColumnViewModel item)
{
var index = Columns.IndexOf(item);
Columns.Move(index, index - 1);
MoveUpCommand.RaiseCanExecuteChanged();
MoveDownCommand.RaiseCanExecuteChanged();
}
private void MoveDown(ColumnViewModel item)
{
var index = Columns.IndexOf(item);
Columns.Move(index, index + 1);
MoveUpCommand.RaiseCanExecuteChanged();
MoveDownCommand.RaiseCanExecuteChanged();
}
}
public class ColumnViewModel: ViewModelBase
{
private bool _isPrimaryKey;
public bool IsPrimaryKey
{
get { return _isPrimaryKey; }
set
{
_isPrimaryKey = value;
NotifyPropertyChange(() => IsPrimaryKey);
}
}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
NotifyPropertyChange(() => IsSelected);
}
}
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
NotifyPropertyChange(() => Name);
}
}
private List<SortOrder> _sortOrders;
public List<SortOrder> SortOrders
{
get { return _sortOrders ?? (_sortOrders = Enum.GetValues(typeof(SortOrder)).OfType<SortOrder>().ToList()); }
}
private SortOrder _sortOrder;
public SortOrder SortOrder
{
get { return _sortOrder; }
set
{
_sortOrder = value;
NotifyPropertyChange(() => SortOrder);
}
}
}
public enum SortOrder {Unsorted, Ascending, Descending}
}
This is what it looks like in my screen:
As you can see in the above example, I am in no way manipulating or creating UI elements in code, because it's actually not necessary. Whenever you need to interact with the pieces of information displayed in the screen, you interact with the ViewModels and not the View. This is the clear separation of concerns between UI and application logic WPF makes possible, which is utterly absent in other frameworks. Please consider this approach the de-facto default when doing any kind o N-element UIs in WPF.
Edit:
Advantages of this approach versus the classic one:
No need to manipulate complex WPF classes (I.E UI elements) in your code in order to show / get data from screen (just simple, simple
properties and INotifyPropertyChanged)
Scales better (UI can be anything as long as it honors the ViewModel properties, you could change the ComboBox to a rotating 3d
pink elephant with a Sort order in each foot.
No need to navigate the visual tree to find elements located God knows where.
No need to foreach anything. Just a simple Select that converts your data (from whatever data source you obtained it) to the
ViewModel list.
Bottom line: WPF is much simpler and nicer than anything else currently in existence, if you use the WPF approach.
Here is a quick example of using an ItemsControl to do what you are wanting:
ViewModel
public class ListBoxViewModel
{
private static readonly List<string> sortList = new List<string>() { "Unsorted", "Sorted" };
public List<string> SortList { get { return sortList; } }
public ObservableCollection<ItemDetail> ItemDetails { get; set; }
#region Up Command
ICommand upCommand;
public ICommand UpCommand
{
get
{
if (upCommand == null)
{
upCommand = new RelayCommand(UpExecute);
}
return upCommand;
}
}
private void UpExecute(object param)
{
var id = param as ItemDetail;
if (id != null)
{
var curIndex = ItemDetails.IndexOf(id);
if (curIndex > 0)
ItemDetails.Move(curIndex, curIndex - 1);
}
}
#endregion Up Command
#region Down Command
ICommand downCommand;
public ICommand DownCommand
{
get
{
if (downCommand == null)
{
downCommand = new RelayCommand(DownExecute);
}
return downCommand;
}
}
private void DownExecute(object param)
{
var id = param as ItemDetail;
if (id != null)
{
var curIndex = ItemDetails.IndexOf(id);
if (curIndex < ItemDetails.Count-1)
ItemDetails.Move(curIndex, curIndex + 1);
}
}
#endregion Down Command
public ListBoxViewModel()
{
ItemDetails = new ObservableCollection<ItemDetail>()
{
new ItemDetail() { IsSelected = false, ItemName = "Customer Id", SortOrder = "Unsorted" },
new ItemDetail() { IsSelected = true, ItemName = "Customer Name", SortOrder = "Sorted" },
new ItemDetail() { IsSelected = false, ItemName = "Customer Age", SortOrder = "Unsorted" }
};
}
}
ItemDetail Class (Made up by me to make things easier)
public class ItemDetail
{
public bool IsSelected { get; set; }
public string ItemName { get; set; }
public string SortOrder { get; set; }
}
XAML
<UserControl.Resources>
<DataTemplate DataType="{x:Type vm:ItemDetail}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="CheckBoxGroup" />
<ColumnDefinition SharedSizeGroup="ItemNameGroup" />
<ColumnDefinition SharedSizeGroup="SortGroup" />
<ColumnDefinition Width="20" />
<ColumnDefinition SharedSizeGroup="UpArrowGroup" />
<ColumnDefinition SharedSizeGroup="DownArrowGroup" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<CheckBox Grid.Column="0" IsChecked="{Binding IsSelected}" VerticalAlignment="Center" />
<Label Grid.Column="1" Content="{Binding ItemName}" />
<ComboBox Grid.Column="2" ItemsSource="{Binding DataContext.SortList, RelativeSource={RelativeSource AncestorType={x:Type views:ListBoxExample}}}" SelectedItem="{Binding SortOrder}" />
<Button Grid.Column="4" Command="{Binding DataContext.UpCommand, RelativeSource={RelativeSource AncestorType={x:Type views:ListBoxExample}}}" CommandParameter="{Binding}">
<Image Source="..\images\up.png" Height="10" />
</Button>
<Button Grid.Column="5" Command="{Binding DataContext.DownCommand, RelativeSource={RelativeSource AncestorType={x:Type views:ListBoxExample}}}" CommandParameter="{Binding}">
<Image Source="..\images\down.png" Height="10" />
</Button>
</Grid>
</DataTemplate>
</UserControl.Resources>
<Grid Grid.IsSharedSizeScope="True">
<ItemsControl ItemsSource="{Binding ItemDetails}" />
</Grid>
And finally the results:
And after pressing the down arrow on the first item:
Hope this helps.
You are changing the order of the RowDefinitions, which is not what you want. You want to change the assignment of elements to rows, which is determined by the Grid.Row attached property
I would put all controls that belong to each row in a container (one per row) and then use Grid.SetRow to change the containers around. See how to change the grid row of the control from code behind in wpf.
Do not know if this is specific to the Infragistics xamDataGrid but here goes the question:
Infragistics xamDataGrid exposes a property IsSynchronizedWithCurrentItem, which according to their documentation, synchronizes ActiveRecord with current item of a datasource that implements ICollectionView.
I have the following MasterDetails window with details (ContentControl) content based on the type of objects bound to the grid:
<DockPanel Name="dockPanel" LastChildFill="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="5" MaxHeight="5"/>
<RowDefinition/>
</Grid.RowDefinitions>
<igDP:XamDataGrid
Name="dataGrid"
IsSynchronizedWithCurrentItem="True"
SelectedItemsChanged="dataGrid_SelectedItemsChanged">
</igDP:XamDataGrid>
<GridSplitter
Style="{StaticResource blueHrizontalGridSplitter}"
Grid.Row="1" Grid.ColumnSpan="2"
BorderThickness="1" Margin="1,0"
HorizontalAlignment="Stretch" />
<ContentControl Grid.Row="2" Name="contentControl" />
</Grid>
</DockPanel>
In code behind, I am attempting to establish a link between the current item of the grid's data source to the DataContext of the details control in my MasterDetailsWindow's constructor as follows:
if (detailsControl != null)
{
var fwDControl = detailsControl as FrameworkElement;
if (fwDControl != null)
{
var b = new Binding() { ElementName = "dataGrid", Path = new PropertyPath("DataSource") };
fwDControl.SetBinding(DataContextProperty, b);
}
contentControl.Content = detailsControl;
}
else
{
var b = new Binding() { ElementName = "dataGrid", Path = new PropertyPath("DataSource") };
contentControl.SetBinding(ContentProperty, b);
b = new Binding("DataDetailsTemplate");
contentControl.SetBinding(ContentTemplateProperty, b);
}
When constructing a instance of the MasterDetails, the caller needs to provide either a detailsControl object or a string representing the URL to DataTemplate. If a detailsControl is provided, I execute code that checks if details is not null. Otherwise, I assume DataDetailsTemplate is provided instead.
I would have doubted my thinking here but if I construct an instance of the MasterDetails window, with a URL that resolves to the following dataTemplate:
<DataTemplate x:Key="LogDetailsTemplate">
<Grid Margin="5,5,5,0">
<TextBox Text="{Binding Message}" TextWrapping="WrapWithOverflow"/>
</Grid>
</DataTemplate>
selecting an item in the grid, displays the selected object's corresponding Message property in the TextBox.
However, if I provide a custom detailsControl object that derives from UserControl, selecting an item in the grid, does not cause change the DataContext of my detailsControl. Why is this?
TIA.
Whoa there!!!!! I may be wrong but it looks like you've come from a WinForms background and are trying to to do things in WPF the way you would for WinForms.
The good news is, you don't have to: Master detail can be handled using a simple forwardslash. In the example below, look at the bindings in MainWindow.xaml - the forwardslash indicates the currently selected item.
MODELS
public class Country
{
public string Name { get; set; }
public int Population { get; set; }
}
public class Continent
{
public string Name { get; set; }
public int Area { get; set; }
public IList<Country> Countries { get; set; }
}
VIEWMODELS
public class MainViewModel
{
private ObservableCollection<ContinentViewModel> _continents;
public ObservableCollection<ContinentViewModel> Continents
{
get { return _continents; }
set
{
_continents = value;
ContinentView = new ListCollectionView(_continents);
ContinentView.CurrentChanged += (sender, agrs) => CurrentContinent = ContinentView.CurrentItem as ContinentViewModel;
}
}
public ListCollectionView ContinentView {get; private set;}
/// <summary>
/// Use this to determine the current item in the list
/// if not willing to use \ notation in the binding.
/// </summary>
public ContinentViewModel CurrentContinent { get; set; }
}
public class ContinentViewModel
{
private Continent _model;
public Continent Model
{
get { return _model; }
set
{
_model = value;
Countries = _model.Countries
.Select(p => new CountryViewModel { Model = p })
.ToList();
}
}
public string Name
{
get { return Model.Name; }
}
public int Area
{
get { return Model.Area; }
}
public List<CountryViewModel> Countries { get; private set; }
}
public class CountryViewModel
{
public Country Model { get; set; }
public string Name
{
get { return Model.Name; }
}
public int Population
{
get { return Model.Population; }
}
}
MainWindow.xaml
<Window x:Class="XamDataGridMasterDetail.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Views="clr-namespace:XamDataGridMasterDetail.Views"
xmlns:igDP="http://infragistics.com/DataPresenter"
Title="MainWindow">
<Grid>
<StackPanel Orientation="Vertical">
<!-- Continent list -->
<igDP:XamDataGrid HorizontalAlignment="Left"
Margin="10,10,0,0"
Name="xamDataGrid1"
Height="300"
VerticalAlignment="Top"
DataSource="{Binding ContinentView}"
IsSynchronizedWithCurrentItem="True">
<igDP:XamDataGrid.FieldSettings>
<igDP:FieldSettings CellClickAction="SelectRecord" />
</igDP:XamDataGrid.FieldSettings>
<igDP:XamDataGrid.FieldLayouts>
<igDP:FieldLayout>
<igDP:FieldLayout.Settings>
<igDP:FieldLayoutSettings AutoGenerateFields="False" />
</igDP:FieldLayout.Settings>
<igDP:FieldLayout.Fields>
<igDP:Field Name="Name"
Label="Name" />
<igDP:Field Name="Area"
Label="Area" />
<igDP:UnboundField Label="# Countries"
Binding="{Binding Countries.Count}" />
</igDP:FieldLayout.Fields>
</igDP:FieldLayout>
</igDP:XamDataGrid.FieldLayouts>
</igDP:XamDataGrid>
<!-- Continent detail -->
<ListBox ItemsSource="{Binding ContinentView/Countries}"
DisplayMemberPath="Name"
IsSynchronizedWithCurrentItem="True"
Height="200" />
<!-- Country detail -->
<StackPanel Orientation="Horizontal">
<Label Content="Name: " />
<TextBlock Text="{Binding ContinentView/Countries/Name}" />
<Label Content="Population: " />
<TextBlock Text="{Binding ContinentView/Countries/Population}" />
</StackPanel>
</StackPanel>
</Grid>
</Window>
App.xaml.cs
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Windows;
using XamDataGridMasterDetail.ViewModels;
using System.Collections.ObjectModel;
using XamDataGridMasterDetail.Model;
namespace XamDataGridMasterDetail
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
{
base.OnSessionEnding(e);
}
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var view = new MainWindow();
var vm = new MainViewModel();
vm.Continents = new ObservableCollection<ContinentViewModel>();
vm.Continents.Add(new ContinentViewModel
{
Model = new Continent
{
Name = "Australasia",
Area = 100000,
Countries = new[]
{
new Country
{
Name="Australia",
Population=100
},
new Country
{
Name="New Zealand",
Population=200
}
}
}
});
vm.Continents.Add(new ContinentViewModel
{
Model = new Continent
{
Name = "Europe",
Area = 1000000,
Countries = new[]
{
new Country
{
Name="UK",
Population=70000000
},
new Country
{
Name="France",
Population=50000000
},
new Country
{
Name="Germany",
Population=75000000
}
}
}
});
view.DataContext = vm;
view.Show();
}
}
}