Reputation: 507
I am attempting to make a UserControl to create a CheckBoxList in WPF using MVVM. Also, Entity Framework is being used to deploy the data. Given the following:
WPF (UserControl)
<Grid>
<ListBox Name="ListBox" ItemsSource="{Binding TheList}" >
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Sport}"
Tag="{Binding SportsId}"
IsChecked="{Binding IsChecked}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
Classes
public class Athlete
{
public int AthleteId { get; set; }
public string Name { get; set; }
public ICollection<Sports> Sports { get; set; }
}
public class Sports {
public int SportsId { get; set; }
public string Sport { get; set; }
}
How can I get the UserControl to load the entire list of the Sports class and then select the ones that the Athlete can play?
Upvotes: 1
Views: 1969
Reputation: 507
I found the solution to my issue. I was able to find it here. It goes like this:
WPF UserControl.xaml
<UserControl x:Class="YourNamespace.CheckBoxList"
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:YourNamespace"
mc:Ignorable="d"
x:Name="ThisCheckBoxList"
d:DesignHeight="450" d:DesignWidth="800">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel>
<ItemsControl x:Name="host"
ItemsSource="{Binding ElementName=ThisCheckBoxList, Path=ItemsSource}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:MyCheckBox x:Name="theCheckbox"
DisplayMemberPath="{Binding ElementName=ThisCheckBoxList, Path=DisplayPropertyPath}"
Unchecked="MyCheckBox_Checked"
Checked="MyCheckBox_Checked"
Tag="{Binding Path=.}">
<local:MyCheckBox.IsChecked >
<MultiBinding Mode="OneWay" >
<MultiBinding.Converter>
<local:IsCheckedValueConverter />
</MultiBinding.Converter>
<Binding Path="."></Binding>
<Binding ElementName="ThisCheckBoxList" Path="SelectedItems"></Binding>
<Binding ElementName="ThisCheckBoxList" Path="DisplayPropertyPath"></Binding>
</MultiBinding>
</local:MyCheckBox.IsChecked>
</local:MyCheckBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</UserControl>
WPF UserControl.xaml.cs
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace Eden
{
/// <summary>
/// Interaction logic for CheckBoxList.xaml
/// </summary>
public partial class CheckBoxList : UserControl
{
public CheckBoxList()
{
InitializeComponent();
}
public object ItemsSource
{
get => GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
// Using a DependencyProperty as the backing store for ItemSource. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(object), typeof(CheckBoxList),
new UIPropertyMetadata(null, (sender, args) => Debug.WriteLine(args)));
public IList SelectedItems
{
get => (IList)GetValue(SelectedItemsProperty);
set => SetValue(SelectedItemsProperty, value);
}
// Using a DependencyProperty as the backing store for SelectedItems. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems", typeof(IList), typeof(CheckBoxList),
new UIPropertyMetadata(null, SelectedChanged));
/// <summary>
/// This is called when selected property changed.
/// </summary>
/// <param name="obj"></param>
/// <param name="args"></param>
private static void SelectedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (args.NewValue is INotifyCollectionChanged ncc)
{
ncc.CollectionChanged += (sender, e) =>
{
CheckBoxList thiscontrol = (CheckBoxList)obj;
RebindAllCheckbox(thiscontrol.host);
};
}
}
private static void RebindAllCheckbox(DependencyObject de)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(de); i++)
{
DependencyObject dobj = VisualTreeHelper.GetChild(de, i);
if (dobj is CheckBox cb)
{
var bexpression = BindingOperations.GetMultiBindingExpression(cb, MyCheckBox.IsCheckedProperty);
if (bexpression != null) bexpression.UpdateTarget();
}
RebindAllCheckbox(dobj);
}
}
public string DisplayPropertyPath
{
get => (string)GetValue(DisplayPropertyPathProperty);
set => SetValue(DisplayPropertyPathProperty, value);
}
// Using a DependencyProperty as the backing store for DisplayPropertyPath. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DisplayPropertyPathProperty =
DependencyProperty.Register("DisplayPropertyPath", typeof(string), typeof(CheckBoxList),
new UIPropertyMetadata("", (sender, args) => Debug.WriteLine(args)));
private PropertyInfo mDisplayPropertyPathPropertyInfo;
private void MyCheckBox_Checked(object sender, RoutedEventArgs e)
{
if (SelectedItems == null)
return;
MyCheckBox chb = (MyCheckBox)sender;
object related = chb.Tag;
if (mDisplayPropertyPathPropertyInfo == null)
{
mDisplayPropertyPathPropertyInfo =
related.GetType().GetProperty(
DisplayPropertyPath, BindingFlags.Instance | BindingFlags.Public);
}
object propertyValue;
if (DisplayPropertyPath == ".")
propertyValue = related;
else
propertyValue = mDisplayPropertyPathPropertyInfo.GetValue(related, null);
if (chb.IsChecked == true)
{
if (!SelectedItems.Cast<object>()
.Any(o => propertyValue.Equals(
DisplayPropertyPath == "." ? o : mDisplayPropertyPathPropertyInfo.GetValue(o, null))))
{
SelectedItems.Add(related);
}
}
else
{
object toDeselect = SelectedItems.Cast<object>()
.Where(o => propertyValue.Equals(DisplayPropertyPath == "." ? o : mDisplayPropertyPathPropertyInfo.GetValue(o, null)))
.FirstOrDefault();
if (toDeselect != null)
{
SelectedItems.Remove(toDeselect);
}
}
}
}
public class MyCheckBox : CheckBox
{
public string DisplayMemberPath
{
get => (string)GetValue(DisplayMemberPathProperty);
set => SetValue(DisplayMemberPathProperty, value);
}
// Using a DependencyProperty as the backing store for DisplayMemberPath. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DisplayMemberPathProperty =
DependencyProperty.Register("DisplayMemberPath",
typeof(string),
typeof(MyCheckBox),
new UIPropertyMetadata(string.Empty, (sender, args) =>
{
MyCheckBox item = (MyCheckBox)sender;
Binding contentBinding = new Binding((string)args.NewValue);
item.SetBinding(ContentProperty, contentBinding);
}));
}
}
BaseMultiValueConverter
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Markup;
namespace Eden
{
/// <summary>
/// A base value converter that allows direct XAML usage
/// </summary>
/// <typeparam name="T">The type of this value converter</typeparam>
public abstract class BaseMultiValueConverter<T> : MarkupExtension, IMultiValueConverter
where T : class, new()
{
#region Private Variables
/// <summary>
/// A single static instance of this value converter
/// </summary>
private static T Coverter = null;
#endregion
#region Markup Extension Methods
/// <summary>
/// Provides a static instance of the value converter
/// </summary>
/// <param name="serviceProvider">The service provider</param>
/// <returns></returns>
public override object ProvideValue(IServiceProvider serviceProvider)
{
return Coverter ?? (Coverter = new T());
}
#endregion
#region Value Converter Methods
/// <summary>
/// The method to convert on type to another
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public abstract object Convert(object[] value, Type targetType, object parameter, CultureInfo culture);
/// <summary>
/// The method to convert a value back to it's source type
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public abstract object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture);
#endregion
}
}
IMultiValueConverter
using EcoDev.Data;
using System;
using System.Collections;
using System.Globalization;
using System.Reflection;
using System.Windows.Data;
namespace Eden
{
/// <summary>
///
/// </summary>
public class IsCheckedValueConverter : BaseMultiValueConverter<IsCheckedValueConverter>
{
private PropertyInfo PropertyInfo { get; set; }
private Type ObjectType { get; set; }
public override object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (values[1] == null) return false; // IF I do not have no value for selected simply return false
if (!(values[2] is string PropertyName)) return false;
if (string.IsNullOrEmpty(PropertyName)) return false;
if (!targetType.IsAssignableFrom(typeof(bool))) throw new NotSupportedException("Can convert only to boolean");
IEnumerable collection = values[1] as IEnumerable;
object value = values[0];
if (value.GetType() != ObjectType)
{
PropertyInfo = value.GetType().GetProperty(PropertyName, BindingFlags.Instance | BindingFlags.Public);
ObjectType = value.GetType();
}
foreach (var obj in collection)
{
if (PropertyName == ".")
{
if (value.Equals(obj)) return true;
}
else
{
if (PropertyInfo.GetValue(value, null).Equals(PropertyInfo.GetValue(obj, null))) return true;
}
}
return false;
}
public override object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
And then all you have to do in whatever window/page you want to use it in is use this code:
<local:CheckBoxList Height="Auto"
SelectedItems="{Binding SelectedItems}"
ItemsSource="{Binding ItemsSource}"
DisplayPropertyPath="Text"/>
Upvotes: 3
Reputation: 17095
The question is very broad and vague but I try to explain the best I can. You may need to read the whole thing at least twice. And also read the external link to the end of it or at least carefully read the codes in it.
First look at the final solution:
public class AthleteVM : DependencyObject
{
public int AthleteId { get; set; }
public string Name { get; set; }
private ObservableCollection<SportSelectionVM> _sports = new ObservableCollection<SportSelectionVM>();
public ObservableCollection<SportSelectionVM> Sports { get { return _sports; } }
}
public class SportSelectionVM : DependencyObject
{
public int SportsId { get; set; }
public string Name { get; set; }
private Model.Sport _model;
public SportSelectionVM(Model.Sport model, bool isSelected)
{
_model = model;
SportsId = model.Id;
Name = model.Name;
IsSelected = isSelected;
}
/// <summary>
/// Gets or Sets IsSelected Dependency Property
/// </summary>
public bool IsSelected
{
get { return (bool)GetValue(IsSelectedProperty); }
set { SetValue(IsSelectedProperty, value); }
}
public static readonly DependencyProperty IsSelectedProperty =
DependencyProperty.Register("IsSelected", typeof(bool), typeof(AthleteVM), new PropertyMetadata(false, (d, e) =>
{
// PropertyChangedCallback
var vm = d as SportSelectionVM;
var val = (bool)e.NewValue;
AthleteDataService.UpdateModel(vm._model, val);//database changes here
}));
}
XAML:
<ListBox Name="ListBox" ItemsSource="{Binding Sports}" >
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Name}"
Tag="{Binding SportsId}"
IsChecked="{Binding IsSelected}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
DataContext of this view is an instance of AthleteVM
. add all sports to Sports
in AthleteVM
and set IsSelected
on those necessary.
see the constructor: public SportSelectionVM(Model.Sport model, bool isSelected)
similar strategy should be used to create an AthleteVM
or to populate AthleteVM list in its parent.
As we know here's the idea behind MVVM:
[Model] <--- [VM] <--TwoWay Binding--> [View]
When EF is added to this pattern, it is usually recommended to follow UOW pattern as well.
Typically the UOW (UnitOfWork) is an object which is in charge of one database transaction (I don't mean SQLTransaction) and it is recommended to always create a UOW inside a using
statement so that it would be disposed afterwards. Using this approach, you should expect to run into this question: how different UOWs interact with each others. which answer is: they don't.
Each UOW creates a lazy copy of the database and starts to modify it until you tell it to discard or save. If another UOW is created in the middle of this process, it doesn't contain any of the changes made to previous UOWs, unless the previous UOW is saved.
So you don't have to worry about Model and instead, you'll focus on the DataService to have something like this.
Considering all this information, ViewModel simply uses an instance of a DataService to fetch data from database and puts them in bindable properties and observable collections to maintain a TwoWay Binding.
But VM and Model do not have a TwoWay relationship, meaning that any change to ViewModel should be reflected on Model and then be saved in database manually.
My favourite solution is to take full advantage of PropertyChangedCallback
feature of DependencyProperty
to tell the DataService to reflect the change:
public int MyProperty
{
get { return (int)GetValue(MyPropertyProperty); }
set { SetValue(MyPropertyProperty, value); }
}
public static readonly DependencyProperty MyPropertyProperty =
DependencyProperty.Register("MyProperty", typeof(int), typeof(MyViewModel),
new PropertyMetadata(0, (d,e)=>
{
var vm = d as MyViewModel;
var val = (int)e.NewValue;//check conditions here
vm._model.MyProperty = val;//update model
vm._dataService.Update(vm._model);//update database
}));
in above sample, class MyViewModel
has an instance of _model
and _dataService
.
Upvotes: 1