Connect UIElements dynamically added in WPF using MVVM - c#

I have UserControl with ItemsControl binded to ObservableCollection. DataTemplate in this ItemsControl is a Grid containing TextBox and Button.
Here is some code (Updated):
<UserControl.Resources>
<entities:SeparatingCard x:Key="IdDataSource"/>
</UserControl.Resources>
<ItemsControl ItemsSource="{Binding Cards}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBox Text="{Binding Id, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" GotFocus="TextBox_GotFocus" Grid.Row="0" Grid.Column="0"/>
<Button DataContext="{Binding Source={StaticResource IdDataSource}}" Command="{Binding Accept}" Grid.Row="0" Grid.Column="1">Accept</Button>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
In model file:
public ObservableCollection<SeparatingCard> Cards { get; set; }
Card class:
class SeparatingCard : INotifyPropertyChanged
{
private string _id;
public string Id
{
get { return _id; }
set
{
_id = value;
OnPropertyChanged("Id");
}
}
public ActionCommand Accept { get; }
public SeparatingCard()
{
Accept = new ActionCommand(AcceptCommandExecute);
}
private void AcceptCommandExecute(object obj)
{
MessageBox.Show(Id);
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Cards are added in runtime and I dynamically get a new textbox-button pair in my UserControl. Now in each pair I need to do the folowing things:
- Be able to check if the text in textbox is correct and disable/enable apropriate button.
- On button click get the text from apropriate textbox and process it.
I'd like all of this done via MVVM. But I only came to solution that directly have access to UI and implements only the second task:
private void Button_Click(object sender, RoutedEventArgs e)
{
var text = (((sender as Button).Parent as Grid).Children
.Cast<UIElement>()
.First(x => Grid.GetRow(x) == 0 && Grid.GetColumn(x) == 0) as TextBox).Text;
MessageBox.Show(text);
}
Update
As was suggested I tried to move ICommand logic to SeparatingCard class. Now it's always return null and I can't check what object of SeparatingCard class my command refers to. Updates are in the code above.

Instead of using Button.Click, Use Button.Command, which you can bind to some command in SeparatingCard.
Please have a look in this tutorial:
http://www.codeproject.com/Tips/813345/Basic-MVVM-and-ICommand-Usage-Example
Then, SeparatingCard ViewModel will contain an ICommand object which you can bind to Button.Command.
So if the user clicks the button, the event will be directed to the corresponding SeparatingCard object's command.

Related

WPF Complex Logic of Custom Controls with MVVM

I am creating a WPF-based plugin (for Revit, an architectural 3D modelling software, but this shouldn't matter) which is quite complex and I'm getting kind of lost.
The WPF Window is composed by 2 tabs and each Tab is a custom UserControl that I'm inserting in the TabItem through a Frame. The Main Window has a ViewModel where the data is bound.
One of the tabs helps with the creation of floors in a 3D model
part of MainWindow.xaml
<TabItem Name="LevelsTab" Header="Levels" HorizontalContentAlignment="Left">
<ScrollViewer >
<Frame Name="LevelsContent" Source="LevelsTab.xaml"/>
</ScrollViewer>
</TabItem>
The LevelsTab.xaml UserControl is really barebone and just contains buttons to create or remove a custom UserControl I created to represent graphically a floor in the UI (screenshot below). This very simple as well:
LevelDefinition.xaml
<UserControl x:Class="RevitPrototype.Setup.LevelDefinition" ....
<Label Grid.Column="0" Content="Level:"/>
<TextBox Name="LevelName" Text={Binding <!--yet to be bound-->}/>
<TextBox Name="LevelElevation" Text={Binding <!--yet to be bound-->}/>
<TextBox Name="ToFloorAbove" Text={Binding <!--yet to be bound-->}/>
</UserControl>
When the user clicks the buttons to add or remove floors in LevelsTab.xaml, a new LevelDefinition is added or removed to the gird.
Each LevelDefinition will be able to create a Level object from the information contained in the different TextBox elements, using MVVM. Eventually, in the ViewModel, I should have a List<Level> I guess.
Level.cs
class Level
{
public double Elevation { get; set; }
public string Name { get; set; }
public string Number { get; set; }
}
Each LevelDefinition should be sort of bound to the previous one though, as the floor below contains the information of the height to the Level above. The right-most TextBox in LevelDefinition.xaml indicated the distance between the current floor and the floor above, hence the Height `TextBox should just be the sum of its height PLUS the distance to the level above:
Of course the extra level of difficulty here is that if I change distance to the level above in one floor, all the floors above will have to update the height. For example: I change LEVEL 01 (from the pic) to have 4 meters to the level above, LEVEL 02's height will have to update to become 7m (instead of 6) and LEVEL 03's will have to become 10m.
But at this point I'm very lost:
How do I get this logic of getting the floor height bound to the info in the floor below?
How do I implement MVVM correctly in this case?
I hope I managed to explain the situation correctly even though it's quite complex and thanks for the help!
If you intend to make your Level items editable, you have to implement INotifyPropertyChanged. I created a level view model for demonstration purposes and added a property OverallElevation that represents the current elevation including that of previous levels.
public class LevelViewModel : INotifyPropertyChanged
{
private string _name;
private int _number;
private double _elevation;
private double _overallElevation;
public LevelViewModel(string name, int number, double elevation, double overallElevation)
{
Number = number;
Name = name;
Elevation = elevation;
OverallElevation = overallElevation;
}
public string Name
{
get => _name;
set
{
if (_name == value)
return;
_name = value;
OnPropertyChanged();
}
}
public int Number
{
get => _number;
set
{
if (_number == value)
return;
_number = value;
OnPropertyChanged();
}
}
public double Elevation
{
get => _elevation;
set
{
if (_elevation.CompareTo(value) == 0)
return;
_elevation = value;
OnPropertyChanged();
}
}
public double OverallElevation
{
get => _overallElevation;
set
{
if (_overallElevation.CompareTo(value) == 0)
return;
_overallElevation = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
You can bind these properties to your LevelDefinition user control. I adapted your sample, because it is incomplete. Since the overall elevation is calculated, I set the corresponding TextBox to be read-only, but you should really use a TextBlock or a similar read-only control instead.
<UserControl x:Class="RevitPrototype.Setup.LevelDefinition"
...>
<UserControl.Resources>
<Style TargetType="{x:Type TextBox}" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="Margin" Value="5"/>
</Style>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="Level:"/>
<TextBox Grid.Column="1" Name="LevelName" Text="{Binding Name}"/>
<TextBox Grid.Column="2" Name="LevelElevation" Text="{Binding OverallElevation}" IsReadOnly="True"/>
<TextBox Grid.Column="3" Name="ToFloorAbove" Text="{Binding Elevation}"/>
</Grid>
</UserControl>
Since you did not provide your tab view model, I created one for reference. This view model exposes an ObservableCollection of levels, a GroundFloor property and commands to add and remove levels. I use a DelegateCommand type, but you may use a different one.
On each add of a level, you subscribe to the PropertyChanged event of the new level and on removal you unsubscribe to prevent memory leaks. Now, whenever a property changes on a LevelViewModel instance, the OnLevelPropertyChanged method is called. This method checks, if the Elevation property was changed. If it was, the UpdateOverallElevation method is called, which recalculates all overall elevation properties. Of course you could optimize this to only recalculate the levels above the current one passed as sender.
For a more robust implementation, you should subscribe to the CollectionChanged event of the Levels collection, so can subscribe to and unsubscribe from the PropertyChanged events of level items whenever you add, remove or modify the collection in other ways than through the commands like restoring a persisted collection.
public class LevelsViewModel
{
private const string GroundName = "GROUND FLOOR";
private const string LevelName = "LEVEL";
public ObservableCollection<LevelViewModel> Levels { get; }
public LevelViewModel GroundFloor { get; }
public ICommand Add { get; }
public ICommand Remove { get; }
public LevelsViewModel()
{
Levels = new ObservableCollection<LevelViewModel>();
GroundFloor = new LevelViewModel(GroundName, 0, 0, 0);
Add = new DelegateCommand<string>(ExecuteAdd);
Remove = new DelegateCommand(ExecuteRemove);
GroundFloor.PropertyChanged += OnLevelPropertyChanged;
}
private void ExecuteAdd(string arg)
{
if (!double.TryParse(arg, out var value))
return;
var lastLevel = Levels.Any() ? Levels.Last() : GroundFloor;
var number = lastLevel.Number + 1;
var name = GetDefaultLevelName(number);
var overallHeight = lastLevel.OverallElevation + value;
var level = new LevelViewModel(name, number, value, overallHeight);
level.PropertyChanged += OnLevelPropertyChanged;
Levels.Add(level);
}
private void ExecuteRemove()
{
if (!Levels.Any())
return;
var lastLevel = Levels.Last();
lastLevel.PropertyChanged -= OnLevelPropertyChanged;
Levels.Remove(lastLevel);
}
private void OnLevelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(LevelViewModel.Elevation))
return;
UpdateOverallElevation();
}
private static string GetDefaultLevelName(int number)
{
return $"{LevelName} {number:D2}";
}
private void UpdateOverallElevation()
{
GroundFloor.OverallElevation = GroundFloor.Elevation;
var previousLevel = GroundFloor;
foreach (var level in Levels)
{
level.OverallElevation = previousLevel.OverallElevation + level.Elevation;
previousLevel = level;
}
}
}
The view for the levels tab item could look like below. You can use a ListBox with your LevelDefinition user control as item template to display the levels. Alternatively, you could use a DataGrid with editable columns for each property of the LevelViewModel, which would be more flexible for users.
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListView ItemsSource="{Binding Levels}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<local:LevelDefinition/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListView>
<DockPanel Grid.Row="1" Margin="5">
<Button DockPanel.Dock="Right" Content="-" MinWidth="50" Command="{Binding Remove}"/>
<Button DockPanel.Dock="Right" Content="+" MinWidth="50" Command="{Binding Add}" CommandParameter="{Binding Text, ElementName=NewLevelElevationTextBox}"/>
<TextBox x:Name="NewLevelElevationTextBox" MinWidth="100"/>
</DockPanel>
<local:LevelDefinition Grid.Row="2" DataContext="{Binding GroundFloor}"/>
</Grid>
This is a simplified example, there is no input validation, invalid values are ignored on adding.
I've managed to implement this using a multi-binding converter.
Assuming that you set up the multi-converter as a static resource somewhere, the TextBlock to display the value is:
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource ElevationMultiConverter}">
<MultiBinding.Bindings>
<Binding Path="" />
<Binding Path="DataContext.Levels" RelativeSource="{RelativeSource AncestorType={x:Type ItemsControl}}" />
</MultiBinding.Bindings>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
The converter itself looks like this:
class ElevationMultiConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var item = values[0] as Level;
var list = values[1] as IList<Level>;
var lowerLevels = list.Where(listItem => list.IndexOf(listItem) <= list.IndexOf(item));
var elevation = lowerLevels.Sum(listItem => listItem.Height);
return elevation.ToString();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
In this example, it depends on the specific order of items in the list to determine whether a level is above or below another; you could use a property, or whatever else.
I didn't use a framework for this example so I needed to implement INotifyPropertyChanged everywhere myself. In the MainViewModel, this meant adding a listener to each Level element's PropertyChanged event to trigger the multibinding converter to have 'changed'. In total, my MainViewModel looked like this:
class MainViewModel :INotifyPropertyChanged
{
public ObservableCollection<Level> Levels { get; set; }
public MainViewModel()
{
Levels = new ObservableCollection<Level>();
Levels.CollectionChanged += Levels_CollectionChanged;
}
private void Levels_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
foreach(var i in e.NewItems)
{
(i as Level).PropertyChanged += MainViewModel_PropertyChanged;
}
}
private void MainViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Levels)));
}
public event PropertyChangedEventHandler PropertyChanged;
}
How it works:
A new Level is added to the collection, and it's PropertyChanged event is listened to by the containing view model. When the height of a level changes, the PropertyChanged event is fired and is picked up by the MainViewModel. It in turn fires a PropertyChanged event for the Levels property. The MultiConverter is bound to the Levels property, and all changes for it trigger the converters to re-evaluate and update all of the levels combined height values.

How to have a button and a combo box together

I want to have a combo box with a button that looks like this:
As I want to use this so that items can be selected and added to a ListView.
Issues:
I don't know how to get and icon in the button like shown
How do you get them to line up really well or is there a way to combine the two elements that I am unaware of?
Here's a working example.
Let's suppose your user control has two controls; a ComboBox and a Button. You want to be able to bind something from your main (parent) to the user control. Then upon selecting something and clicking the button, you want user control to notify to the parent of the event occurrence, and also pass the selected value.
The UserControl XAML:
<UserControl ...
d:DesignHeight="40" d:DesignWidth="200">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="40"/>
</Grid.ColumnDefinitions>
<ComboBox Grid.Column="0" Margin="4" Name="ItemsComboBox"
ItemsSource="{Binding Source, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
<Button Grid.Column="1" Margin="4" Content="+"
Click="Button_Click"/>
</Grid>
</UserControl>
The following binding will allow you to bind a list of data to the combo box, form the parent:
ItemsSource="{Binding Source, RelativeSource={RelativeSource AncestorType=UserControl}}"
From your MainWindow, you'll use the control like so:
<Grid>
<local:UCComboButton Grid.Row="0" Width="200" Height="40" x:Name="MyUC"
Source="{Binding Names}"/>
</Grid>
And in the UserControls code behind:
public partial class UCComboButton : UserControl
{
public UCComboButton()
{
InitializeComponent();
}
// We use this dependency property to bind a list to the combo box.
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(IEnumerable), typeof(UCComboButton), new PropertyMetadata(null));
public IEnumerable Source
{
get { return (IEnumerable)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
// This is to send the occurred event, in this case button click, to the parent, along with the selected data.
public class SelectedItemEventArgs : EventArgs
{
public string SelectedChoice { get; set; }
}
public event EventHandler<SelectedItemEventArgs> ItemHasBeenSelected;
private void Button_Click(object sender, RoutedEventArgs e)
{
var selected = ItemsComboBox.SelectedValue;
ItemHasBeenSelected?.Invoke(this, new SelectedItemEventArgs { SelectedChoice = selected.ToString() });
}
}
Now in the MainWindow.xaml.cs:
public MainWindow()
{
InitializeComponent();
// Subscribe to the item selected event
MyUC.ItemHasBeenSelected += UCButtonClicked;
Names = new List<string>
{
"A",
"B",
"C"
};
DataContext = this;
}
void UCButtonClicked(object sender, UCComboButton.SelectedItemEventArgs e)
{
var value = e.SelectedChoice;
// Do something with the value
}
Note that the above Names list is what's bound to the user control from the main window XAML.

Selection change/add items with ComboBox in WPF

Simple questions can be the hardest sometimes. 3 things I am trying to understand;
1. Allow a selection change within a combobox to help populate items in 2nd combobox.
2. Clear items in 2nd box before populating items.
3. Adding items in 2nd box.
Note that this code worked on my WinForms code, but I am trying to convert it to WPF and understand that code.
Code:
<ComboBox Name="ComboBox_Location" HorizontalAlignment="Left" Margin="170,56,0,0" VerticalAlignment="Top" Width="160">
<ComboBoxItem Content="Hospital"/>
</ComboBox>
<ComboBox Name="ComboBox_Printer" HorizontalAlignment="Left" Margin="30,131,0,0" VerticalAlignment="Top" Width="300"/>
$ComboBox_Location.add_SelectionChanged{
switch ($ComboBox_Location.SelectedItem){
"Hospital"{
$ComboBox_Printer.Items.Clear();
$Hospital = Get-Printer -ComputerName \\bmh01-print01 | where {($_.Name -like “*BMH01*”) -and ($_.DeviceType -eq "Print")}
foreach($Name in $Hospital){
$ComboBox_Printer.Items.Add("$($Name.name)");
}
}
}
Thank you in advance! And if any of you have a website or cite I could go to to see the specific coding for WPF, any help will be appreciated!
why is this question not answered by anyone.Anyway I will do my best to explain you. Hope I am not late to answer this question.
In WPF, we follow MVVM pattern, So there are 3 parts Model, View and ViewModel.
In viewmodel, we need to inherit Icommand and create a CommandHandler, so that if there is any button click / Selction changed will sent via this command and the delegated eventhandler will be raised.
The CommandHandler Class
public class CommandHandler : ICommand
{
private Action<object> _action;
private bool _canExeute;
public event EventHandler CanExecuteChanged;
private bool canExeute
{
set
{
_canExeute = value;
CanExecuteChanged(this, new EventArgs());
}
}
public CommandHandler(Action<object> action,bool canExecute)
{
_action = action;
_canExeute = canExecute;
}
public bool CanExecute(object parameter)
{
return _canExeute;
}
public void Execute(object parameter)
{
_action(parameter);
}
}
This CommandHandler will be used in the ViewModel Class, and then the viewmodel will be set as Datacontext to view via XAML.
public class ViewModelBase : INotifyPropertyChanged
{
private List<String> _printer = new List<string>();
private bool _canExecute;
public ViewModelBase()
{
_canExecute = true;
}
public List<string> Printers
{
get { return _printer; }
set { _printer = value; }
}
private ICommand _SelectedItemChangedCommand;
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
public ICommand SelectedItemChangedCommand
{
get
{
return _SelectedItemChangedCommand ?? (_SelectedItemChangedCommand =
new CommandHandler(obj => SelectedItemChangedHandler(obj), _canExecute));
}
}
public void SelectedItemChangedHandler(object param)
{
var selectedItem = ((ComboBoxItem)param).Content;
switch (selectedItem)
{
case "Hospital":
Printers = new List<string>(); //clearing the list;
// Hospital = GetHospital();// - ComputerName \\bmh01 - print01 | where { ($_.Name - like “*BMH01 *”) -and($_.DeviceType - eq "Print")}
// Here I have added data hard coded, you need to call your method and assgin it to printers property.
Printers.Add("First Floor Printer");
Printers.Add("Second Floor Printer");
OnPropertyChanged(nameof(Printers));
break;
default:
Printers = new List<string>();
break;
}
}
}
The ViewModel class is also inheriting INotifyPropertyChanged, where we need to implement the event and raise it. Now we need to raise propertychanged event providing the property name which is changed using assignment. Therefore inside SelectionChangedCommand, we add Printer and then raise PropertyChanged Event by sending the Printers PropertyName in as Parameter.
The View, We can use either Window or UserControl, For this example I have used Window.
View:-
<Window x:Class="Combo2.MainScreen"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:local="clr-namespace:Combo2"
xmlns:ViewModel="clr-namespace:Combo2.Validate"
mc:Ignorable="d"
x:Name="Window1"
Title="MainScreen" Height="450" Width="800">
<Window.DataContext>
<ViewModel:ViewModelBase/>
</Window.DataContext>
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Content="Location" HorizontalAlignment="Left" Grid.Row="0" VerticalAlignment="Top" Grid.Column="0"/>
<ComboBox Name="ComboBox_Location" HorizontalAlignment="Left" VerticalAlignment="Top" Width="160" Grid.Row="0" Grid.Column="1" >
<ComboBoxItem Content="Hospital"/>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding SelectedItemChangedCommand}" CommandParameter="{Binding ElementName=ComboBox_Location, Path=SelectedItem}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ComboBox>
<Label Content="Printer Names" HorizontalAlignment="Left" Grid.Row="1" VerticalAlignment="Top" Grid.Column="0"/>
<ComboBox Name="ComboBox_Printer" HorizontalAlignment="Left" VerticalAlignment="Top" Width="160" Grid.Row="1" Grid.Column="1" ItemsSource="{Binding Printers}" >
</ComboBox>
</Grid>
Now, As in winform we have click or selectionchanged event but in order to keep the designer separate from code, we are not directly coupling it with it. I mean to say write a selection changed event in code behind then we are not making justification to it. For more information click on https://www.tutorialspoint.com/mvvm/mvvm_introduction.htm which will give you more insight into MVVM.
Now if you notice, when there is a selection changed we have binded the Command Property to a Command Property present in the Viewmodel, which is possible using Interaction class.
So where did we link the view and viewmodel that it at the top of the xaml. Here the datacontext is bound to ViewmodelBase class(viewmodel)
<Window.DataContext>
<ViewModel:ViewModelBase/>
</Window.DataContext>
Now answer to your question
1)Allow a selection change within a combobox to help populate items in 2nd combobox.
The selectionChanged event is called which will call the Command method present in the ViewModelBase and popluate the Printers Property.
<i:InvokeCommandAction Command="{Binding SelectedItemChangedCommand}" CommandParameter="{Binding ElementName=ComboBox_Location, Path=SelectedItem}"/>
Now since the viewmodel is bound to view any change to the property is displayed in the 2nd dropdown. Now that I have cleared and added data in Printers property, when the 1st drop is selected based on the text if matches "Hospital" the printers are added to the Property and displayed in 2nd Drop down.
2) Clear items in 2nd box before populating items
Before adding data in Printers property, it is cleared by instantiating the List, in your case it could be any other class. Now to whether the selected data is Hospital, we need to send the SelectedItem using the Command Parameter , we cast the "param" with ComboBoxItem and got the content.
3) Adding items in 2nd box.
We sure did add the values in Printers property.
Hope this helps you !!

mvvm windows phone 8 confusion and how to capture when text is changed

I'm trying to write a windows phone 8 app using the mvvm pattern but I'm struggling with it.
I have a page with a list of persons which is binded to my PersonViewModel. That part is working fine. I then have 2 buttons in the application bar i.e. add or edit. When I want to edit a person, I select the person from the list, which then sets the CurrentPerson in my ViewModel. This in turns set a property in my MainViewModel which is used to store the currently selected person i.e.
App.MainViewModel.CurrentPerson = this.CurrentPerson;
When I want to add a new person, I use the same principal but I create a new person model.
App.MainViewModel.CurrentPerson = new PersonModel();
I then redirect to a page which contains the fields to handle a person, whether it is being added or edited and this is binded to a ViewModel called PersonEntryViewModel
Before I explain my problem, I want to let you know what I'm trying to achieve. I want the "Save" button in my application bar to get enabled once a certain amount of criteria have been met i.e. Name has been filled and has x characters, etc...
I can see what my problem is but I don't know how to resolve it.
Here is a simplied version of my PersonEntryViewModel:
public class PersonEntryViewModel : BaseViewModel
{
private PersonModel _currentPerson;
private bool _isNewPerson;
private ICommand _savePersonCommand;
private ICommand _cancelCommand;
private ICommand _titleTextChanged;
private bool _enableSaveButton;
public PersonEntryViewModel()
{
this.CurrentPerson = App.MainViewModel.CurrentPerson ?? new PersonModel();
}
public ICommand SavePersonCommand
{
get
{
return this._savePersonCommand ?? (this._savePersonCommand = new DelegateCommand(SavePersonAction));
}
}
public ICommand CancelCommand
{
get
{
return this._cancelCommand ?? (this._cancelCommand = new DelegateCommand(CancelAction));
}
}
public ICommand NameTextChanged
{
get
{
return this._nameTextChanged ?? (this._nameTextChanged = new DelegateCommand(NameTextChangedAction));
}
}
private void NameTextChangedAction(object actionParameters)
{
if (!string.IsNullOrEmpty(this._currentPerson.Name) && _currentPerson.Name.Length > 2)
{
EnableSaveButton = true;
}
}
private void CancelAction(object actionParameters)
{
Console.WriteLine("Cancel");
INavigationService navigationService = this.GetService<INavigationService>();
if (navigationService == null)
return;
navigationService.GoBack();
navigationService = null;
}
private void SavePersonAction(object actionParameters)
{
Console.WriteLine("Saving");
}
public PersonModel CurrentPerson
{
get { return this._currentPerson; }
set
{
if (this._currentPerson != value)
this.SetProperty(ref this._currentPerson, value);
}
}
public string PageTitle
{
get { return this._pageTitle; }
set { if (this._pageTitle != value) this.SetProperty(ref this._pageTitle, value); }
}
public bool IsNewPerson
{
get { return this._isNewPerson; }
set
{
if (this._isNewPerson != value)
{
this.SetProperty(ref this._isNewPerson, value);
if (this._isNewPerson)
this.PageTitle = AppResources.PersonEntryPageNewTitle;
else
this.PageTitle = AppResources.PersonEntryPageEditTitle;
}
}
}
public bool EnableSaveButton
{
get { return this._enableSaveButton; }
set { if (this._enableSaveButton != value) this.SetProperty(ref this._enableSaveButton, value); }
}
}
Here is part of my XAML:
<Grid x:Name="LayoutRoot" Background="Transparent" DataContext="{StaticResource PersonEntryViewModel}" >
<!--ContentPanel - place additional content here-->
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0" DataContext="{Binding CurrentPerson, Mode=TwoWay}">
<Grid Margin="0,0,0,5">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border BorderBrush="{StaticResource PhoneAccentBrush}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderThickness="5"
Background="Transparent"
CornerRadius="5">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Text="Name:"
Grid.Row="0"
Margin="12,0,0,0"/>
<TextBox Text="{Binding Name, Mode=TwoWay}"
Grid.Row="1">
<!--<i:Interaction.Triggers>
<i:EventTrigger EventName="TextChanged">
<i:InvokeCommandAction Command="{Binding NameTextChanged, Mode=OneWay}" CommandParameter="{Binding}" />
</i:EventTrigger>
</i:Interaction.Triggers>-->
</TextBox>
<TextBlock Text="Address:"
Grid.Row="2"
Margin="12,0,0,0"/>
<TextBox Text="{Binding Address, Mode=TwoWay}"
AcceptsReturn="True"
Height="200"
VerticalScrollBarVisibility="Visible"
TextWrapping="Wrap"
Grid.Row="3"/>
As you can see, my layoutRoot grid is binded to my ViewModel i.e. PersonEntryViewModel and the grid content panel containing my textboxes required for editing is binded to CurrentPerson.
Is that the correct way to do it? I need to bind the control to the CurrentPerson property which will contain data if the person is being edited and it will contain a new empty PersonModel if a new person is being added.
As it stands, that part is working. When I type some text in my field and click on the next one, it calls set the CurrentPerson relevant property which in turns calls the PersonModel. Click on the save button and I check the CurrentPerson, I can see it has all the various properties set.
As you can see in my PersonEntryViewModel, I've got other properties which are required. For example the EnableSaveButton, which technically should be set to true or false based on the validation of the various properites from the CurrentPerson object but I need this to be checked as the user is typing text in the various textbox and this is where I'm having a problem.
If I enable the following code:
<i:Interaction.Triggers>
<i:EventTrigger EventName="TextChanged">
<i:InvokeCommandAction Command="{Binding NameTextChanged, Mode=OneWay}" CommandParameter="{Binding}" />
</i:EventTrigger>
</i:Interaction.Triggers>
It doesn't get triggered in the PersonEntryViewModel where I really need it as this is where I want to set my EnableSaveButton property but I guess it makes sense as this code is binded to the Name textbox which is turn is binded to the CurrentPerson property which is my PersonModel.
If I move the code from the PersonEntryViewModel to the PersonViewModel
private ICommand _personTextChanged;
public ICommand PersonTextChanged
{
get
{
return this._personTextChanged ?? (this._personTextChanged = new DelegateCommand(PersonTextChangedAction));
}
}
private void PersonTextChangedAction(object actionParameters)
{
if (!string.IsNullOrEmpty(this._name) && this._name.Length > 2)
{
//EnableSaveButton = true;
Console.WriteLine("");
}
}
It gets triggered accordingly but then how do I get this information back to my PersonEntryViewModel which binded to the view where my 2 buttons (i.e. save & cancel) are located and the EnableSaveButton property is responsible for enabling the save button accordingly when set assuming that the Name is valid i.e. set and minlen is match for example.
Is the PersonEntryViewModel and using a CurrentPerson property with the current person being edited or added designed correctly or not and how am I to handle this scenario?
I hope the above makes sense but if I'm not clear about something, let me know and I'll try to clarify it.
Thanks.
PS: I posted another posted related to how to detect text change, but I figured it out but it's obviously not the problem. The problem seems more related to design.
Your design is unclear to me.
If you want to go with the current design itself, I would suggest you do the following thing.
Remove assigning the DataContext for grid in xaml.
In the code behind add:
var dataContext = new PersonEntryViewModel();
this.ContentPanel.DataContext = dataContext.CurrentPerson;
// After creating App bar
this.appBar.DataContext = dataContext;
//Your xaml code will look something like this:
<AppBar x:Name="appBar">
<Button x:Name="saveBtn" IsEnabled={Binding EnableSaveButton} />
<AppBar />

Binding ComboBox SelectedItem to DataContext Value

I need to bind a ComboBox's Selected Item according to a value of a field in the data context which it's parent container receives .
the container is a grid which receives it's datacontext from an item in an itemcontrol when it's clicked
private void Button_Click(object sender , RoutedEventArgs e)
{
GridEmployee.DataContext = ((Button)sender).DataContext;
}
*the Button got it's itemsource from a list of Employee's Bounded to the itemControl
the grid holds some controls amongst them a combobox which i initalize through an Enum
public Enum Gender
{
Male,Female
};
foreach(string _gender in Enum.GetNames(Gender) )
{
GenderComboBox.Items.Add(_gender);
}
the Employee class has a matching Property Gender
private string gender;
public string Gender
{
get{return gender;}
set
{
gender = value ;
if( PropertyChanged != null )
PropertyChanged(this,new PropertyChangedEventArgs("Gender"));
}
}
the GenderComboBox.SelectedItem is bounded to the value of the Gender Property for the bounded object Employee
<ComboBox x:Name="GenderComboBox" SelectedItem="{Binding Gender , Mode=TwoWay}" />
the problem here of course that the item does not get selected ..
I tought may be its becuase the items in the combobox are strings and I try to bound them according to a custom converter which just take the Enum Value and returns the .ToString()
of it .
but I was not able to check this becuase that threw an An XamlParseException in form's contractor .
which I did not fully understand why it happend ,may be becuase it did not have a value to convert when I form loads.
so to conclude how do I bind a Property from My Employee Class
to a combobox with the string representation of the Property's Value?
Works nicely in my case....
XAML
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:tk="http://schemas.microsoft.com/wpf/2008/toolkit"
Title="GenderSelection" Height="100" Width="300" x:Name="MyWindow">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock FontSize="40"
Text="{Binding MyGender, ElementName=MyWindow, Mode=TwoWay}"/>
<ComboBox Grid.Row="1"
ItemsSource="{Binding Genders, ElementName=MyWindow}"
SelectedItem="{Binding MyGender, ElementName=MyWindow, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</Window>
Code Behind
public enum Gender
{
Male,
Female
}
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window, INotifyPropertyChanged
{
private string myGender = Gender.Male.ToString();
public string MyGender
{
get
{
return myGender;
}
set
{
myGender = value;
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs("MyGender"));
}
}
}
public string[] Genders
{
get
{
return Enum.GetNames(typeof(Gender));
}
}
public Window1()
{
InitializeComponent();
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
Let me know if this guides you in correct direction...
Just change the initialization of your ComboBox to
foreach(string _gender in Enum.GetNames(Gender) )
{
GenderComboBox.Items.Add(_gender.ToString());
}
That should work, because your Gender property of the Employees class returns a string.

Categories