I want to implement somekind of messaging communciation (I know how to use messaging of MVVM.Light) but I think my case is trickier, because I'm using the CommandParameter to change ViewModel, I can't add the command I want :x to the code to become more clear.
XAML
<ListView x:Name="dataGrid" ItemsSource="{Binding Friends}" Height="314" BorderThickness="0" SelectedItem="{Binding SelectedItemFriends}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Source="Resources\Images\ic_status.png" Height="24" Width="18"/>
<StackPanel Margin="5" Orientation="Vertical">
<TextBlock FontWeight="Bold" Text="{Binding name}"/>
<StackPanel x:Name="RemoveItems" Margin="5" Orientation="Vertical">
<TextBlock Text="{Binding lastLocation}"/>
<TextBlock Text="{Binding timestamp}"/>
</StackPanel>
<StackPanel x:Name="AdditionItems" Margin="5" Orientation="Vertical" Visibility="Collapsed">
<TextBlock Text="{Binding Path=loc.area}"/>
<TextBlock Text="{Binding Path=loc.building}"/>
<TextBlock Text="{Binding Path=loc.floor}"/>
<TextBlock Text="{Binding Path=loc.room}"/>
</StackPanel>
</StackPanel>
<Button Style="{DynamicResource FlatButtonStyle}" Command="{Binding DataContext.SelectViewCommand, ElementName=GeneralWindowView}" CommandParameter="ChatViewModel" x:Name="button1" Content="Chat" Margin="10">
<Button.Template>
<ControlTemplate>
<Image Source="Resources\Images\chat_image.png"/>
</ControlTemplate>
</Button.Template>
</Button>
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListViewItem}}, Path=IsSelected}" Value="true">
<Setter TargetName="AdditionItems" Property="Visibility" Value="Visible"/>
<Setter TargetName="RemoveItems" Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
So I am in the FriendsViewModel (is a child of GeneralViewModel) and I want to send information to the ChatViewModel (also a child of GeneralViewModel). The thing is where can I fire the command ? Because I'm using the Command Parameter, I can't implement other command :x and use messenger.
Thanks in advance,
EDIT: AViewModel
public abstract class AViewModel : ViewModelBase
{
//public WindowService ws;
public string Name { get; set; }
public RelayCommand<string> SelectViewCommand { get; set; }
public AViewModel()
{
//ws = new WindowService();
SelectViewCommand = new RelayCommand<string>(OnSelectViewCommand);
}
private static ObservableCollection<ViewModelBase> _ViewModels;
public static ObservableCollection<ViewModelBase> ViewModels
{
get { return _ViewModels; }
set { _ViewModels = value; }
}
public void AddViewModel(ViewModelBase viewmodel)
{
if (ViewModels == null)
ViewModels = new ObservableCollection<ViewModelBase>();
var currentVNs = (from vms in ViewModels where vms.InternalName == viewmodel.InternalName select vms).FirstOrDefault();
if (currentVNs == null)
ViewModels.Add(viewmodel);
}
public ViewModelBase GetViewModel(string viewmodel)
{
return ViewModels.FirstOrDefault(item => item.InternalName == viewmodel);
}
public ViewModelBase GetViewModelLogin(string viewmodel,object bla)
{
return ViewModels.FirstOrDefault(item => item.InternalName == viewmodel);
}
private void OnSelectViewCommand(string obj)
{
switch (obj)
{
case "ExitCommand":
Application.Current.Shutdown();
break;
default:
this.Current_ViewModel = this.GetViewModel(obj);
break;
}
}
private ViewModelBase _Current_ViewModel;
private IMessenger _messengerInstance;
public ViewModelBase Current_ViewModel
{
get { return _Current_ViewModel; }
set { _Current_ViewModel = value; OnPropertyChanged("Current_ViewModel"); }
}
protected IMessenger MessengerInstance
{
get
{
return this._messengerInstance ?? Messenger.Default;
}
set
{
this._messengerInstance = value;
}
}
}
If you remember in my last demo I was raising an 'Event' in the setter of property 'Current_ViewModel' in AviewModel that gets fired every time you navigate to a different View. Well, after thinking about it I had the idea that the Event can also send an object [to the new ViewModel\View] also....
you need to use the 'CommandParameter' class tho...
public enum Command
{
None,
LogIn,
LogOut,
Recovery,
Register,
Exit
}
public class CommandParameter
{
public dynamic Obj { get; set; }
public Command Command { get; set; }
public string Link_1 { get; set; }
public string Link_2 { get; set; }
}
example usage...
this.LogInCommandParameter = new CommandParameter() { Obj = this.CurrentUser, Command = Command.LogIn, Link_1 = "Main_ViewModel", Link_2 = "LogOnError_ViewModel" };
in the above code
Obj = the object you want to send onto you next View
Command = is a 'hint' that you can Switch on in your Buttons bound Command
Link_1 & Link_2 are the Views that you want to Navigate too....
there is a lot more to it than that so I have attached another demo here http://www.mediafire.com/download/5ttjhuiuxex7eo1/Navigation1_25-05.rar (file size is quite big now because the Demo uses EF and there is a web service too.... should all work from VS IDE tho)
you'll see in AviewModel
private ViewModelBase _Current_ViewModel;
public ViewModelBase Current_ViewModel
{
get { return _Current_ViewModel; }
set {
if (Current_ViewModel != null)
Current_ViewModel.RaiseDeActivate(SendObject);
_Current_ViewModel = value;
if (Current_ViewModel != null)
Current_ViewModel.RaiseActivate(SendObject);
OnPropertyChanged("Current_ViewModel"); }
}
I am now raising two events (DeActivate and Activate) and also note that I am sending a param called SendObject (this is your Obj in CommandParameter)...
in the ViewModel you simply subscribe to the Events like so....
// in the constructor....
this.Activate += Main_ViewModel_Activate;
this.DeActivate += Main_ViewModel_DeActivate;
private void Main_ViewModel_DeActivate(object sender, ActivateArgs e)
{
}
private void Main_ViewModel_Activate(object sender, ActivateArgs e)
{
// e.Data will be the SendObject from AviewModel
}
Have a look\step though at the Demo code... you'll see that (after creating a new user to log on with) once you log on with a valid UserName\Password, that 'User' (the entire 'User' Object) is passed FROM LogOn_ViewModel TO Main_ViewModel VIA the OnWindowCommand in Base_ViewModel... the UserName (that you used to log on with in LogOn_View) is then displayed on the Main_View.
Related
I'm trying to fill my Combobox ItemsSource using Binding in Xaml with a collection of data ObservableCollection but I always get the Combobox ItemsSource null .
WPF UserControl Cs Code : ( Update the code )
public ObservableCollection<User> users { get; set; }
public partial class MainWindow : Window
{
InitializeComponent();
User user1 = new User("Mariah", 15);
User user2 = new User("John", 19 );
users = new ObservableCollections<User>();
users.Add(user1);
users.Add(user2);
this.DataContext = this;
Console.WriteLine(ComboBoxUsers.Items.Count); // always 0
Console.WriteLine(ComboBoxUsers.ItemsSource); // always null
}
WPF UserControl Xaml Code : ( Updated my code )
<ComboBox SelectedIndex = "0" x:Name="ComboBoxUsers" ItemsSource="{Binding users, UpdateSourceTrigger=PropertyChanged}" FontFamily="Arial" FontSize="11" Grid.Row="3" Height="30" Margin="10,5,5,5">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" >
<Image IsEnabled="False" Source="{Binding Image}"/>
<TextBlock x:Name="textblock" IsEnabled="False" Text="{Binding Name} />
</StackPanel>
<DataTemplate.Resources>
<Style TargetType="ComboBoxItem">
<Setter Property="IsEnable" Value="{Binding IsEnable}"/>
</Style>
</DataTemplate.Resources>
</DataTemplate>
</ComboBox.ItemTemplate>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem" BasedOn="{StaticResource {x:Type ComboBoxItem}}">
<Setter
Property="Visibility"
Value="{Binding IsHidden}" />
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
Class User
public class User
{
public string Name { get; set; }
public int Age { get; set; }
public User(string name, int age)
{
this.Name = name;
this.Age = age;
}
}
What is the source of this problem?
Discarding unnecessary (which is not relevant to the question or has syntax errors), your example with a little formatting works fine.
XAML:
<ComboBox x:Name="ComboBoxUsers"
ItemsSource="{Binding Users}"
FontFamily="Arial"
FontSize="11"
Grid.Row="3"
Height="30"
Margin="10,5,5,5">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" >
<TextBlock IsEnabled="False"
Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Code-behind:
public class User
{
public string Name { get; }
public int Age { get; }
public User(string name, int age)
{
Name = name;
Age = age;
}
}
public partial class MainWindow : Window
{
public ObservableCollection<User> Users { get; }
public MainWindow()
{
InitializeComponent();
// Initialize collection with some items
Users = new ObservableCollection<User>
{
new User("Mariah", 15),
new User("John", 19)
};
DataContext = this;
}
}
Result:
Remarks:
Your
Console.WriteLine(ComboBoxUsers.Items.Count); // always 0
Console.WriteLine(ComboBoxUsers.ItemsSource); // always null
because you use Binding. You needn't access ItemsSource or Items.Count through ComboBox - you should use binded collection Users (e.g. Users.Count) to manipulate or get ComboBox content.
EDIT.
About SelectedItem. You should define for yourself, you want use Bindings or work with ComboBox directly.
Binding push you to NOT use ComboBox.SelectedItem/Index/Value whatever. Even not access ComboBoxUsers to get some data. Binding is closely related to, for example, the MVVM Pattern. If you decided to use Bindings - forget about accessing directly to ComboBox or it data properties SelectedItem/Index/Value or similar.
If you use Bindings - you should create a property (e.g. SelectedUser) for SelectedItem (same as you create property Users for your ItemsSource ) and bind to it:
XAML (introducing binding for SelectedItem property and SelectionChanged handler):
<ComboBox x:Name="ComboBoxUsers"
ItemsSource="{Binding Users}"
SelectedItem="{Binding SelectedUser}"
SelectionChanged="OnUserSelectionChanged"
FontFamily="Arial"
FontSize="11"
Grid.Row="3"
Height="30"
Margin="10,5,5,5">
</ComboBox>
Code-behind (introducing property SelectedUser and OnUserSelectionChanged handler):
public partial class MainWindow : Window
{
public ObservableCollection<User> Users { get; }
// Here would be stored your Binded selected item
public User SelectedUser { get; set; }
// And here is your handler when selection changed
private void OnUserSelectionChanged(object sender, SelectionChangedEventArgs e)
{
// SelectedUser property stores selected in ComboBox item,
// so you can use it directly
_ = MessageBox.Show(SelectedUser.Name);
// Even if you wish to get directly - it is possible
// (thanks to #Clemens):
var selectedUser = (sender as ComboBox).SelectedItem as User;
var selectediIndex = (sender as ComboBox).SelectedIndex;
}
public MainWindow()
{
InitializeComponent();
Users = new ObservableCollection<User>
{
new User("Mariah", 15),
new User("John", 19)
};
DataContext = this;
}
}
Repeat same algorithm for each property you wish to Bind (e.g. SelectedIndex):
SelectedIndex="{Binding SelectedUserIndex}"
public int SelectedUserIndex { get; set; }
Result:
Decide for yourself, what you need. Nice modern Bindings or old boring (sender as ComboBox).SelectedItem.
I am developing an application using c# and the Universal Windows Platform (UWP) and am struggling with creating a one-way data-bind between a layout control and an observable class. Currently, when the observable class property is changed, it does not update the UI element. I think it has something to do with the fact that I am binding a DataTemplate ListViewItem rather than a static layout element, but I am not sure if this is the problem or how to solve it. Any help would be appreciated. The code for the UI element and backend code is shown.
DataTemplate (XAML) (Styling is removed for readability)
<DataTemplate x:Key="variableTemplate"
x:DataType="local:VariableNode">
<Border>
<StackPanel Orientation="Vertical">
<Border>
<Grid>
<TextBlock Text="{Binding Name}" />
<StackPanel Orientation="Horizontal" >
<Button Tag="{Binding Description}"/>
<Button Tag="{Binding}"/>
</StackPanel>
</Grid>
</Border>
<Grid Margin="0, 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Border >
<Grid Grid.Column="0">
<Button Click="Choose_Measurement"
Tag="{Binding}">
<StackPanel Orientation="Vertical">
<TextBlock Text="{x:Bind Path=Measurement_Name, Mode=TwoWay}"
Foreground="{x:Bind MF}" />
<TextBlock Foreground="{x:Bind MF}" />
</StackPanel>
</Button>
</Grid>
</Border>
<Grid Grid.Column="1">
<Button Foreground="{Binding UF}"
Tag="{Binding}"
IsEnabled="{Binding Unit_Exists}"
Click="Choose_Unit">
<StackPanel Orientation="Vertical">
<TextBlock Text="{x:Bind Path=Unit_Name, Mode=OneWay}"
Foreground="{Binding UF}" />
<TextBlock Foreground="{Binding UF}" />
</StackPanel>
</Button>
</Grid>
</Grid>
</StackPanel>
</Border>
</DataTemplate>
C# Observable Class VariableNode (Irrelevant properties removed)
public class VariableNode : ExperimentNode
{
public VariableNode() { }
public VariableNode(VariableType type)
{
Type = type;
Name = name_ref[(int)Type];
Category = "Problem";
Unit = -1;
}
private string[] name_ref = { "Independent Variable", "Dependent Variable", "Controlled Variable" };
public enum VariableType { Independent, Dependent, Controlled };
public VariableType Type { get; set; }
public Measurement Measure { get; set; }
public int Unit { get; set; }
[XmlIgnoreAttribute]
public Measurement MeasureSource
{
get { return this.Measure; }
set
{
this.Measure = value;
OnPropertyChanged("Measurement_Name");
}
}
[XmlIgnoreAttribute]
public string Measurement_Name
{
get
{
if (Measure == null) { return "Select a Measurement"; }
else { return Measure.Name; }
}
set
{
if (Measure != null)
{
Measure.Name = value;
OnPropertyChanged();
}
}
}
[XmlIgnoreAttribute]
public string Unit_Name
{
get
{
if (Measure == null) { return "No measurement"; }
else if (Unit < 0) { return "Select a unit"; }
else { return Measure.Unit[Unit]; }
}
}
[XmlIgnoreAttribute]
public bool Unit_Exists
{
get { return Measure != null; }
}
}
C# XAML.CS code calling the property change
public void Choose_Measurement (object sender, RoutedEventArgs e)
{
Button butt = sender as Button
VariableNode sel = butt.Tag as VariableNode;
sel.Measurement_Name = "New Name";
}
Again thanks for the help, I know its a lot of code, and I appreciate the help in debugging / learning.
Ok, so I ended up finding the answer, and I think that it may help others trying to replicate what I am trying to do:
Basically, the class that one is trying to make observable must extend the class INotifyPropertyChanged. So, I ended up making a base class from which to extend all of my observable classes from:
public class BaseClass : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
protected void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChanged(this, e);
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
}
Self-taught programmer, would love any constructive criticism regarding my code.
I have a ListView that will have ListViewItems that I want to customize.
The ListViewItem I have made has two TextBlocks and a ToggleSwitch. When the ToggleSwitch is switched On/Off I want it to call a method from an instantiate object, or call a method from the same form, but somehow retrieve the object that initially loaded into the DataTemplate.
Here is the XAML so far:
<ListView x:Name="listViewAddedVideoFolders" Grid.Row="1" DoubleTapped="listViewAddedVideoFolders_DoubleTapped" SelectionChanged="listViewAddedVideoFolders_SelectionChanged" HorizontalContentAlignment="Stretch">
<ListView.ItemTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Stretch">
<TextBlock HorizontalAlignment="Stretch" VerticalAlignment="Center" Text="{Binding Directory}"/>
<Grid HorizontalAlignment="Right">
<StackPanel>
<TextBlock Text="Find Videos: "></TextBlock>
<ToggleSwitch Toggled="listViewVideoFolder_toggled" />
</StackPanel>
</Grid>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</ListView.ItemContainerStyle>
Right now it is calling listViewVideoFolder_toggled
Before I was trying to use Toggled="{Binding StartCrawling()}"
Here is the AddVideoFolderModel object that I am binding the listviewitems to
namespace Movie_Management_Windows_10.Models
{
public class AddVideoFolderModel
{
public static ObservableCollection<AddVideoFolderModel> MyVideoFolderModels = new ObservableCollection<AddVideoFolderModel>();
public int VideosFound { get; set; }
public string Directory { get; set; }
public string DirectoryName { get; set; }
private bool isCrawling = false;
public bool HasBeenCrawled = false;
private void startCrawling()
{
AppShell.Current.NotifyUser("Crawling began", AppShell.NotifyType.StatusMessage);
}
//public override string ToString()
//{
// return Directory + " (" + VideosFound.ToString() + ")";
//}
}
}
What must I implement to accomplish this?
At first, you can add property to your model and bind to IsOn property in ToggleSwitch using TwoWay mode binding. Is this case, your model must implement INotifyPropertyChanged
private bool _isNeedCrawle;
public bool IsNeedCrawle
{
get
{
return _isNeedCrawle;
}
set
{
if (_isNeedCrawle != value)
{
_isNeedCrawle = value;
if (_isNeedCrawle)
{
startCrawling();
}
NotifyPropretyChanged("IsNeedCrawle");
}
}
}
At second, you can use XAML Behavior SDK. In this case, you must Add reference to library (look how to do it), and change method modifier from private to public
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:core="using:Microsoft.Xaml.Interactions.Core"
<ToggleSwitch>
<i:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="Toggled">
<core:CallMethodAction MethodName="StartCrawling" TargetObject="{Binding }"/>
</core:EventTriggerBehavior>
</i:Interaction.Behaviors>
</ToggleSwitch>
I have a checklist view that has 2 ScrollViewers. One checklist is for incomplete items, the other is for complete items. They are populated by 2 separate observable collections and bound to by ItemsControls.
The UserControl has a button, when clicked will move that 'check' to the other collection.
Currently the way I have this setup is in the ViewModel that's the DataContext for the UserControl there is a public event that is subscribed to by the main window's VM by using:
((CheckItemVM) ((CheckListItem) cli).DataContext).CompleteChanged += OnCompleteChanged;
where cli is the checklist item.
then the OnCompleteChanged finds the appropriate View object by using:
foreach (object aCheck in Checks)
{
if (aCheck.GetType() != typeof (CheckListItem)) continue;
if (((CheckListItem) aCheck).DataContext == (CheckItemVM) sender)
{
cliToMove = (CheckListItem) aCheck;
break;
}
}
It's pretty obvious this breaks MVVM and I'm looking for a way around it (CheckListItem is the View, and CheckItemVM is it's DataContext ViewModel). Reasoning for the boxed type is I've got another UserControl that will have instances inside both, which are basically section labels, and I need to be able to sort my observable collections where there is an association between the checklistitem to a specific section by name.
This can be done in MVVM using commands, and bindings....
The idea that I propouse here is to create a command in the Windows view model, that manage the check command, and this command to receive the item view model in the params, then manage the the things in the command. I'm going to show you a simple example, using MvvmLight library:
The model:
public class ItemViewModel : ViewModelBase
{
#region Name
public const string NamePropertyName = "Name";
private string _name = null;
public string Name
{
get
{
return _name;
}
set
{
if (_name == value)
{
return;
}
RaisePropertyChanging(NamePropertyName);
_name = value;
RaisePropertyChanged(NamePropertyName);
}
}
#endregion
#region IsChecked
public const string IsCheckedPropertyName = "IsChecked";
private bool _myIsChecked = false;
public bool IsChecked
{
get
{
return _myIsChecked;
}
set
{
if (_myIsChecked == value)
{
return;
}
RaisePropertyChanging(IsCheckedPropertyName);
_myIsChecked = value;
RaisePropertyChanged(IsCheckedPropertyName);
}
}
#endregion
}
A simple model with two property, one for the name (an identifier) and another for the check status.
Now in the Main View Model, (or Windows view model like you want)....
First the Collections, one for the checked items, and another for the unchecked items:
#region UncheckedItems
private ObservableCollection<ItemViewModel> _UncheckedItems;
public ObservableCollection<ItemViewModel> UncheckedItems
{
get { return _UncheckedItems ?? (_UncheckedItems = GetAllUncheckedItems()); }
}
private ObservableCollection<ItemViewModel> GetAllUncheckedItems()
{
var toRet = new ObservableCollection<ItemViewModel>();
foreach (var i in Enumerable.Range(1,10))
{
toRet.Add(new ItemViewModel {Name = string.Format("Name-{0}", i), IsChecked = false});
}
return toRet;
}
#endregion
#region CheckedItems
private ObservableCollection<ItemViewModel> _CheckedItems;
public ObservableCollection<ItemViewModel> CheckedItems
{
get { return _CheckedItems ?? (_CheckedItems = GetAllCheckedItems()); }
}
private ObservableCollection<ItemViewModel> GetAllCheckedItems()
{
var toRet = new ObservableCollection<ItemViewModel>();
foreach (var i in Enumerable.Range(11, 20))
{
toRet.Add(new ItemViewModel { Name = string.Format("Name-{0}", i), IsChecked = true });
}
return toRet;
}
#endregion
And the command:
#region CheckItem
private RelayCommand<ItemViewModel> _CheckItemCommand;
public RelayCommand<ItemViewModel> CheckItemCommand
{
get { return _CheckItemCommand ?? (_CheckItemCommand = new RelayCommand<ItemViewModel>(ExecuteCheckItemCommand, CanExecuteCheckItemCommand)); }
}
private void ExecuteCheckItemCommand(ItemViewModel item)
{
//ComandCode
item.IsChecked = true;
UncheckedItems.Remove(item);
CheckedItems.Add(item);
}
private bool CanExecuteCheckItemCommand(ItemViewModel item)
{
return true;
}
#endregion
The magic here could be in the Data binding, in this case I used command parameter and the FindAncestor binding, check the Data Template:
<DataTemplate x:Key="UncheckedItemDataTemplate">
<Grid>
<StackPanel Orientation="Horizontal">
<TextBlock HorizontalAlignment="Left" TextWrapping="Wrap" Text="{Binding Name}" VerticalAlignment="Top"/>
<CheckBox HorizontalAlignment="Left" VerticalAlignment="Top" IsChecked="{Binding IsChecked}" IsEnabled="False"/>
<Button Content="Check" Width="75" Command="{Binding DataContext.CheckItemCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}" CommandParameter="{Binding Mode=OneWay}"/>
</StackPanel>
</Grid>
</DataTemplate>
<DataTemplate x:Key="CheckedItemDataTemplate">
<Grid>
<StackPanel Orientation="Horizontal">
<TextBlock HorizontalAlignment="Left" TextWrapping="Wrap" Text="{Binding Name}" VerticalAlignment="Top"/>
<CheckBox HorizontalAlignment="Left" VerticalAlignment="Top" IsChecked="{Binding IsChecked}" IsEnabled="False"/>
</StackPanel>
</Grid>
</DataTemplate>
One data template for checked items, and another for unchecked items. Now the usage, this is simpler:
<ListBox Grid.Row="2" Margin="5" ItemsSource="{Binding UncheckedItems}" ItemTemplate="{DynamicResource UncheckedItemDataTemplate}"/>
<ListBox Grid.Row="2" Margin="5" Grid.Column="1" ItemsSource="{Binding CheckedItems}" ItemTemplate="{DynamicResource CheckedItemDataTemplate}"/>
This is a cleaner solution, hope is helps.
I have an ItemsControl with an item template that contains two ComboBoxes. For any given item, the second ComboBox is required iff the first ComboBox has a selected value. I have set this validation up using IDataErrorInfo on the view model.
Rather than flagging ComboBox #2 as invalid the second a user selects a value in ComboBox1, I want to perform the validation when the user tries to save. It's kind of annoying to have a form "yell" at you for doing something wrong on a field you haven't even had a chance to enter yet.
Normally you could force this validation by retrieving the BindingExpression for the ComboBox and calling UpdateSource() and then determine if there is an error by calling Validation.GetHasError() passing the ComboBox. Since the ComboBoxes are generated dynamically by the ItemsControl, it is not as easy to get to. So I have 2 questions: 1. How do you ensure validation has executed for all controls when the save button is clicked. 2. How do you check whether there are validation errors when the save button is clicked. Validation.GetHasError remains false for the ItemsControl even when a ComboBox2 within it has an error. Thanks.
EDIT:
I had followed this article to implement IDataErrorInfo in order to validate the combobox properties relative to each other.
public class IntroViewModel : INotifyPropertyChanged, IDataErrorInfo
{
public Guid ClassScheduleID
{
get { return _intro.ClassScheduleID; }
set
{
_intro.ClassScheduleID = value;
OnPropertyChanged("ClassScheduleID");
//OnPropertyChanged("TrialDate"); //This will trigger validation on ComboBox2 when bound ComboBox1 changes
}
}
public DateTime TrialDate
{
get { return _intro.TrialDate; }
set
{
_intro.TrialDate = value;
OnPropertyChanged("TrialDate");
}
}
public string Error
{
get { return null; }
}
public string this[string columnName]
{
get { return ValidateProperty(columnName); }
}
private string ValidateProperty(string propertyName)
{
string error = null;
switch (propertyName)
{
case "TrialDate":
if (_intro.TrialDate == DateTime.MinValue && _intro.ClassScheduleID != Guid.Empty)
error = "Required";
break;
default:
error = null;
break;
}
return error;
}
}
I attempted to create the behavior you need based on some assumptions
sample
XAML
<StackPanel>
<StackPanel Orientation="Horizontal">
<Button Command="{Binding AddItem}"
Content="Add Item" />
<Button Command="{Binding Save}"
Content="Save" />
</StackPanel>
<ItemsControl ItemsSource="{Binding Data}"
Grid.IsSharedSizeScope="True">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border x:Name="border"
BorderThickness="1"
Padding="2"
Margin="2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="value1" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ComboBox Text="{Binding Value1}"
ItemsSource="{Binding Source={StaticResource sampleData}}" />
<ComboBox Text="{Binding Value2}"
ItemsSource="{Binding Source={StaticResource sampleData}}"
Grid.Column="1" />
</Grid>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsValid}"
Value="False">
<Setter TargetName="border"
Property="BorderBrush"
Value="Red" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
Main VM
public ViewModel()
{
AddItem = new SimpleCommand(i => Data.Add(new DataViewModel(new DataModel())));
Save = new SimpleCommand(i =>
{
foreach (var vm in Data)
{
vm.ValidateAndSave();
}
}
);
Data = new ObservableCollection<DataViewModel>();
}
public ObservableCollection<DataViewModel> Data { get; set; }
public ICommand AddItem { get; set; }
public ICommand Save { get; set; }
data VM and model
public class DataModel
{
public object Value1 { get; set; }
public object Value2 { get; set; }
}
public class DataViewModel : INotifyPropertyChanged
{
DataModel model;
public DataViewModel(DataModel model)
{
this.model = model;
IsValid = true;
}
object _value1;
public object Value1
{
get
{
return _value1;
}
set
{
_value1 = value;
}
}
object _value2;
public object Value2
{
get
{
return _value2;
}
set
{
_value2 = value;
}
}
public bool IsValid { get; set; }
public void ValidateAndSave()
{
IsValid = !(_value1 != null && _value2 == null);
PropertyChanged(this, new PropertyChangedEventArgs("IsValid"));
if (IsValid)
{
model.Value1 = _value1;
model.Value2 = _value2;
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
so the VM will validate all the items when you click save and will save only those items which are valid. otherwise will mark the IsValid property to false which will be notified to UI
I can't tell how you've implemented the IDataErrorInfo interface in your code, but in my implementation, doing what you want is simple. For future users, you can find out about this interface on the IDataErrorInfo Interface page on MSDN. On the linked page, you will see that you need to implement the Item indexer and the Error property.
That's all you need, because if you have implemented it correctly, then you can find out if your data (implementing) item has an error by simply checking the value of the Error property:
bool hasError = string.IsNullOrEmpty(yourDataTypeInstance.Error);
if (!hasError) Save(yourDataTypeInstance);
else MessageBox.Show("Invalid data!");
UPDATE >>>
Try using this instead:
public DateTime TrialDate
{
get { return _intro.TrialDate; }
set
{
_intro.TrialDate = value;
OnPropertyChanged("TrialDate");
OnPropertyChanged("Error");
}
}
public string Error
{
get { return this["TrialDate"]; }
}
I'll leave you to work out the rest, which is essentially managing strings.
Here is how I accomplished it while waiting for answers. When a save is intiated, ValidateTrials() is called to ensure validation has fired for the comboboxes and then TrialsHaveErrors() is called to check whether there are validation errors on them. This is the brute force approach I'd like to avoid, but it does work.
//Force validation on each combobox2
private void ValidateTrials()
{
foreach (IntroViewModel introVm in icTrials.Items)
{
ContentPresenter cp = (ContentPresenter)icTrials.ItemContainerGenerator.ContainerFromItem(introVm);
if (cp == null) continue;
ComboBox cb2 = (ComboBox)cp.ContentTemplate.FindName("cb2", (FrameworkElement)cp);
//Update the source to force validation.
cb2.GetBindingExpression(ComboBox.SelectedValueProperty).UpdateSource();
}
}
//Recursively searches the Visual Tree for ComboBox elements and checks their errors state
public bool TrialsHaveError(DependencyObject ipElement)
{
if (ipElement!= null)
{
for (int x = 0; x < VisualTreeHelper.GetChildrenCount(ipElement); x++)
{
DependencyObject child = VisualTreeHelper.GetChild(ipElement, x);
if (child != null && child is ComboBox)
{
if (Validation.GetHasError(child))
return true;
}
if (TrialsHaveError(child)) return true; //We found a combobox with an error
}
}
return false;
}
Slimmed down XAML:
<ItemsControl Name="icTrials" ItemsSource="{Binding Intros}" Margin="10,6,10,0" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid Grid.Row="2">
<ComboBox Name="cb1"
SelectedValuePath="ID"
SelectedValue="{Binding Path=ClassScheduleID, Converter={StaticResource nullEmptyConverter}, ConverterParameter=System.Guid}"
ItemsSource="{Binding ClassesSource}">
<ComboBox.ItemTemplate>
<DataTemplate>
...
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ComboBox Name="cb2"
ItemsSource="{Binding AvailableStartDates}"
DisplayMemberPath="Date"
ItemStringFormat="{}{0:d}"
SelectedValue="{Binding Path=TrialDate, Converter={StaticResource nullEmptyConverter}, ConverterParameter=System.DateTime, ValidatesOnDataErrors=True}">
</ComboBox>
</Grid>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
To avoid the issue of flagging the field invalid before the user has had a chance to set it, I updated the setter for cb1's bound property, ClassScheduleID to conditionally fire notification for the TrialDate property depending on how the value is changing.