WPF: How to Create ViewModel Within the View - wpf

Essentially, I have a markup issue. I have come up with a few solutions but I can't help but feel like this should be simpler. Rather than lead you down my convoluted path I thought I would share the simplest implementation and ask how you would address it.
MainPage.xaml
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="6" />
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="6" />
<ColumnDefinition />
<!--Additional Columns-->
</Grid.ColumnDefinitions>
<!--Row Definitions-->
<Label Grid.Row="0" Grid.Column="0" Content="Vin:" HorizontalAlignment="Right" />
<ctrl:CommandTextBox Grid.Row="0" Grid.Column="2" Command="{Binding CreateVehicleCommand}" CommandParameter="{Binding Text, RelativeSource={RelativeSource Self}}" />
<Label Grid.Row="0" Grid.Column="3" Content="Manufacturer:" HorizontalAlignment="Right" />
<TextBox Grid.Row="0" Grid.Column="5" IsEnabled="False" Text="{Binding Vehicle.Manufacturer, Mode=OneWay}" />
<!--Additional Read Only Values-->
</Grid>
Given the example above, how can I get the Contents of the Grid into a View given the constraint that the Command to create the vehicle is outside of the DataContext to be created(Vehicle)?
If you do wish to look at my specific attempt, that question is here UserControl's DependencyProperty is null when UserControl has a DataContext

how can I get the Contents of the Grid into a View given the
constraint that the Command to create the vehicle is outside of the
DataContext to be created(Vehicle)?
This feels like a race condition more than an MVVM problem. I will address the issue first but make a secondary suggestion after.
There are no reasons in which a ViewModel cannot contain another viewmodel as a reference and that reference is bound to using the INotifyPropertyChanged mechanisim.
Or that your xaml (view) page contains a static reference to a ViewModel which the page (view) does not directly use in its DataContext, but that a certain control cannot bind to that static outside of the data context of the containing control.
Either way one can provide access (as also mentioned in the response to the other post you provided) by pointing to itself to get data or to provide an alternate plumbing which gets the data.
Or you can flatten your viewmodel to contain more information and handle this IMHO race condition so that this situation doesn't arise and the control as well as the grid can access information in a proper format.
I can't fully address the problem because you are more aware of the design goals and hazards which now must be worked around.

I've come up with something, I'm relatively happy with. This has saved me from creating 100s of composite ViewModel's and while it does introduce some unnecessary complexity it does dramatically reduce the amount copy/paste code I need to write.
VMFactoryViewModel.cs
public class CreatedViewModelEventArgs<T> : EventArgs where T : ViewModelBase
{
public T ViewModel { get; private set; }
public CreatedViewModelEventArgs(T viewModel)
{
ViewModel = viewModel;
}
}
public class VMFactoryViewModel<T> : ViewModelBase where T : ViewModelBase
{
private Func<string, T> _createViewModel;
private RelayCommand<string> _createViewModelCommand;
private readonly IDialogService _dialogService;
/// <summary>
/// Returns a command that creates the view model.
/// </summary>
public ICommand CreateViewModelCommand
{
get
{
if (_createViewModelCommand == null)
_createViewModelCommand = new RelayCommand<string>(x => CreateViewModel(x));
return _createViewModelCommand;
}
}
public event EventHandler<CreatedViewModelEventArgs<T>> CreatedViewModel;
private void OnCreatedViewModel(T viewModel)
{
var handler = CreatedViewModel;
if (handler != null)
handler(this, new CreatedViewModelEventArgs<T>(viewModel));
}
public VMFactoryViewModel(IDialogService dialogService, Func<string, T> createViewModel)
{
_dialogService = dialogService;
_createViewModel = createViewModel;
}
private void CreateViewModel(string viewModelId)
{
try
{
OnCreatedViewModel(_createViewModel(viewModelId));
}
catch (Exception ex)
{
_dialogService.Show(ex.Message);
}
}
}
VMFactoryUserControl.cs
public class VMFactoryUserControl<T> : UserControl where T : ViewModelBase
{
public static readonly DependencyProperty VMFactoryProperty = DependencyProperty.Register("VMFactory", typeof(VMFactoryViewModel<T>), typeof(VMFactoryUserControl<T>));
public VMFactoryViewModel<T> VMFactory
{
get { return (VMFactoryViewModel<T>)GetValue(VMFactoryProperty); }
set { SetValue(VMFactoryProperty, value); }
}
}
GenericView.xaml
<ctrl:VMFactoryUserControl x:Class="GenericProject.View.GenericView"
x:TypeArguments="vm:GenericViewModel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctrl="clr-namespace:SomeProject.Controls;assembly=SomeProject.Controls"
xmlns:vm="clr-namespace:GenericProject.ViewModel">
<Grid>
<!-- Column Definitions -->
<!-- Row Definitions -->
<Label Grid.Row="0" Grid.Column="0" Content="Generic Id:" HorizontalAlignment="Right" />
<ctrl:CommandTextBox Grid.Row="0" Grid.Column="2"
Command="{Binding VMFactory.CreateViewModelCommand, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
CommandParameter="{Binding Text, RelativeSource={RelativeSource Self}}" />
<Label Grid.Row="0" Grid.Column="3" Content="Generic Property:" HorizontalAlignment="Right" />
<TextBox Grid.Row="0" Grid.Column="5" IsEnabled="False" Text="{Binding GenericProperty, Mode=OneWay}" />
<!--Additional Read Only Values-->
</Grid>
</ctrl:VMFactoryUserControl>
GenericView.xaml.cs
public partial class GenericView : VMFactoryUserControl<GenericViewModel>
{
public GenericView()
{
InitializeComponent();
}
}
MainPageViewModel.cs
public class MainPageViewModel : ViewModelBase
{
private readonly IDialogService _dialogService;
private GenericViewModel _generic;
private readonly VMFactoryViewModel<GenericViewModel> _genericFactory;
public GenericViewModel Generic
{
get { return _generic; }
private set
{
if (_generic != value)
{
_generic = value;
base.OnPropertyChanged("Generic");
}
}
}
public VMFactoryViewModel<GenericViewModel> GenericFactory
{
get { return _genericFactory; }
}
private void OnGenericFactoryCreatedViewModel(object sender, CreatedViewModelEventArgs<GenericViewModel> e)
{
Generic = e.ViewModel;
}
public MainPageViewModel(IDialogService dialogService)
{
_dialogService = dialogService;
_genericFactory = new VMFactoryViewModel<GenericViewModel>(_dialogService, x => new GenericViewModel(_dialogService, GetGeneric(x)));
_genericFactory.CreatedViewModel += OnGenericFactoryCreatedViewModel;
}
private Generic GetGeneric(string genericId)
{
// Return some Generic model.
}
}
MainPage.xaml
<Page x:Class="GenericProject.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vw="clr-namespace:GenericProject.View">
<StackPanel>
<!-- Headers and Additional Content. -->
<vw:EventView DataContext="{Binding Generic}"
VMFactory="{Binding DataContext.GenericFactory, RelativeSource={RelativeSource AncestorType={x:Type Page}}}" />
</StackPanel>
</Page>

Related

How to Register an Attached Property of Textbox Text So Datagrid Can Update on Change

I have a textbox and a datagrid like so:
<Page
TextElement.FontSize="14" FontFamily="Segoe UI"
Title="Delivery View">
<Page.Resources>
<xcdg:DataGridCollectionViewSource x:Key="firstNameDataSource"
Source="{Binding Path=Accessor.Views[FirstNameView].SourceCollection}"
AutoFilterMode="And"
DistinctValuesConstraint="Filtered">
<xcdg:DataGridCollectionViewSource.ItemProperties>
<xcdg:DataGridItemProperty Name="FirstName" CalculateDistinctValues="False"/>
</xcdg:DataGridCollectionViewSource.ItemProperties>
</xcdg:DataGridCollectionViewSource>
</Page.Resources>
<ScrollViewer Name="pendingScroll" HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible">
<DockPanel Name="pnlMainPanel" LastChildFill="True" Style="{StaticResource panelBackground}">
<Grid Margin="15">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" FontSize="18" Text="Pending Guests" Margin="0,1,3,1" Foreground="SteelBlue" HorizontalAlignment="Left"/>
<TextBox Name="txtFirstNameFilter" Grid.Row="1" >
</TextBox>
<xcdg:DataGridControl x:Name="gridPendingGuests" Margin="5,0,5,1"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
MinHeight="100"
MinWidth="200"
CellEditorDisplayConditions="None"
EditTriggers="None"
ItemScrollingBehavior="Immediate"
AutoCreateColumns="False"
SelectionMode="Single"
NavigationBehavior="RowOnly"
ItemsSource="{Binding Source={StaticResource firstNameDataSource}}">
<xcdg:DataGridControl.View>
<xcdg:TableView ShowRowSelectorPane="False"/>
</xcdg:DataGridControl.View>
<xcdg:DataGridControl.Columns>
<xcdg:Column x:Name="FirstName" FieldName="FirstName" Title="First Name" Width="150" />
</xcdg:DataGridControl.Columns>
<i:Interaction.Behaviors>
<utils:UpdateDataGridOnTextboxChange/>
</i:Interaction.Behaviors>
</xcdg:DataGridControl>
</Grid>
</DockPanel>
</ScrollViewer>
</Page>
In the datagrid, you have a collection of first names. This works perfectly. The display is good. As you can see, I added an Interactions.Behavior class which currently handles a filter with a hard coded value when the user clicks on the datagrid with their mouse. The filtering works fine. If there is a first name of "John", that record is removed from view, leaving all other records in place.
Here is that code:
using System.Windows.Interactivity;
using System.Windows;
using Xceed.Wpf.DataGrid;
using System;
namespace Some.Namespace.Behaviors
{
public class UpdateDataGridOnTextboxChange : Behavior<DataGridControl>
{
protected override void OnAttached()
{
AssociatedObject.MouseUp += AssociatedObjectOnMouseUp;
base.OnAttached();
}
protected override void OnDetaching()
{
AssociatedObject.MouseUp -= AssociatedObjectOnMouseUp;
base.OnDetaching();
}
private void AssociatedObjectOnMouseUp(object sender, RoutedEventArgs routedEventArgs)
{
var items = AssociatedObject.Items;
items.Filter = CollectionFilter;
}
private bool CollectionFilter(object item)
{
System.Data.DataRow dr = item as System.Data.DataRow;
//set the ItemArray as Guest
Guest guest = SetGuest(dr);
if (guest.FirstName.Equals("John"))
{
return false;
}
return true;
}
private Guest SetGuest(System.Data.DataRow dr)
{
Guest guest = new Guest();
guest.FirstName = dr.ItemArray[0].ToString();
return guest;
}
public class Guest
{
public string FirstName { get; set; }
}
}
}
This works as expected. Again, when the user clicks on the datagrid, the filter filters out the users with the First Name of "John".
What I WANT to have happen is for the user to be able to type a first name in the txtFirstNameFilter Textbox and the datagrid to then filter the records that contain the text in the first name, keeping them visible and the others without that first name to not be visible.
The way I can do it is with an attached property of the Textbox TextChanged property? That's a question, because I don't know how to do an attached property and then how to make sure that when that attached property actually changes, call the AssociatedObjectOnMouseUp method to run the filtering.
System.Windows.Interactivity.Behavior<T> inherits from DependencyObject. So give it a dependency property and bind that.
public class UpdateDataGridOnTextboxChange : Behavior<DataGrid>
{
#region FilterValue Property
public String FilterValue
{
get { return (String)GetValue(FilterValueProperty); }
set { SetValue(FilterValueProperty, value); }
}
public static readonly DependencyProperty FilterValueProperty =
DependencyProperty.Register(nameof(FilterValue), typeof(String), typeof(UpdateDataGridOnTextboxChange),
new FrameworkPropertyMetadata(null, FilterValue_PropertyChanged));
protected static void FilterValue_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
(d as UpdateDataGridOnTextboxChange).OnFilterValueChanged(e.OldValue);
}
private void OnFilterValueChanged(object oldValue)
{
// Do whatever you do to update the filter
// I did a trace just for testing.
System.Diagnostics.Trace.WriteLine($"Filter value changed from '{oldValue}' to '{FilterValue}'");
}
#endregion FilterValue Property
/*****************************************
All your code here
*****************************************/
}
XAML:
<i:Interaction.Behaviors>
<utils:UpdateDataGridOnTextboxChange
FilterValue="{Binding Text, ElementName=txtFirstNameFilter}"
/>
</i:Interaction.Behaviors>
You should rename it, though. It's got nothing to do with text boxes. You could bind FilterValue to a viewmodel property, or the selected value in a ComboBox, or whatever.
Update
OP's having trouble with the binding only updating FilterValue when the text box loses focus. This isn't what I'm seeing, but I don't know what's different between the two.
There isn't any UpdateTargetTrigger property of Binding, but you can swap the source and the target when both are dependency properties of dependency objects. This works for me:
<TextBox
x:Name="txtFirstNameFilter"
Text="{Binding FilterValue, ElementName=DataGridFilterThing, UpdateSourceTrigger=PropertyChanged}"
/>
<!-- snip snip snip -->
<i:Interaction.Behaviors>
<local:UpdateDataGridOnTextboxChange
x:Name="DataGridFilterThing"
/>
</i:Interaction.Behaviors>

Binding Error in runtime clearing ObservableCollection

I have an ObservableCollection that works perfectly, but I can't remove a binding error that appears in runtime when I clear this ObservableCollection:
System.Windows.Data Information: 21 : BindingExpression cannot retrieve value from null data item. This could happen when binding is detached or when binding to a Nullable type that has no value. BindingExpression:Path=Icon; DataItem='NamedObject' (HashCode=40835417); target element is 'Image' (Name=''); target property is 'Source' (type 'ImageSource')
I made a small code to reproduce my problem that I show below: (View):
<Button Height="40" Width="40" Click="Button_Click"></Button>
<ListBox ItemsSource="{Binding ProductList}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid IsItemsHost="True"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image Height="32" Source="{Binding Icon}" Stretch="Fill" Width="32"/>
<Label Grid.Row="1" Content="{Binding Description}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
public ObservableCollection<Product> ProductList { get; set; }
public void TestList()
{
ProductList = new ObservableCollection<Product>();
ProductList.Add(new Product("Product1", "pack://application:,,,/Product1.png"));
ProductList.Add(new Product("Product2", "pack://application:,,,/Product2.png"));
ProductList.Add(new Product("Product3", "pack://application:,,,/Product3.png"));
}
private void Button_Click(object sender, RoutedEventArgs e)
{
ProductList.Clear();
ProductList.Add(new Product("Product4", "pack://application:,,,/Product4.png"));
}
And my product class:
public class Product : INotifyPropertyChanged
{
#region "## INotifyPropertyChanged Members ##"
public event PropertyChangedEventHandler _propertyChanged;
public event PropertyChangedEventHandler PropertyChanged
{
add { this._propertyChanged += value; }
remove { this._propertyChanged -= value; }
}
protected virtual void OnPropertyChanged(string propertyName)
{
App.Current.Dispatcher.BeginInvoke((Action)delegate
{
if (this._propertyChanged != null)
this._propertyChanged(this, new PropertyChangedEventArgs(propertyName));
});
}
#endregion
public string Description
{
get { return this.description; }
set
{
this.description = value;
this.OnPropertyChanged("Description");
}
}
private string description;
public BitmapImage Icon
{
get { return this.icon; }
set
{
this.icon = value;
this.OnPropertyChanged("Icon");
}
}
private BitmapImage icon;
public Product(string desc, string iconPath)
{
Description = desc;
BitmapImage bi = new BitmapImage(new Uri(iconPath));
bi.Freeze();
Icon = bi;
}
}
The error appears when I click the button and the following line is executed:
ProductList.Clear();
I have done many tests:
Individually delete items from the list
Use a Fallback and TargetNullValue:
<Image Height="32" Source="{Binding Icon, FallbackValue='pack://application:,,,/transparent.png', TargetNullValue='pack://application:,,,/transparent.png'}" Stretch="Fill" Width="32"/>
Any ideas?
Try removing the TargetNullValue, and FallbackValue and see if it still occurs? If it does not there is a problem with your URI to the file.
Also keep in mind if you are referencing transparent.png often you are loading the same image into memory many times. Instead consider adding a line to your ResourceDictionary like so:
<BitmapImage UriSource="/MyApp;component/Images/transparent.png" x:Key="Transparent" PresentationOptions:Freeze="True" />
Then using in the XAML like so:
<Image Height="32" Source="{Binding Icon, FallbackValue={StaticResource:Transparent}, TargetNullValue={StaticResource:Transparent}" Stretch="Fill" Width="32"/>
This change will load your image once but use it many places decreasing memory pressure.

Upgrade from ListBox to CollectionViewSource not working

I already have a working ListBox with Items from my local database. Now I wanted to upgrade this to a CollectionViewSource for filtering. After my upgrade the new ListBox with CollectionViewSource shows nothing.
MainPage Code Behind:
// Data context for the local database
private BuildingDataContext toDoDB;
// Define an observable collection property that controls can bind to.
private ObservableCollection<Building> _buildings;
public ObservableCollection<Building> BuildingTable
{
get
{
return _buildings;
}
set
{
if (_buildings != value)
{
_buildings = value;
NotifyPropertyChanged("BuildingTable");
}
}
}
public CollectionViewSource Source { get; set; }
// Konstruktor
public MainPage()
{
InitializeComponent();
// Connect to the database and instantiate data context.
toDoDB = new BuildingDataContext(BuildingDataContext.DBConnectionString);
// Data context and observable collection are children of the main page.
this.DataContext = this;
}
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
// Define the query to gather all of the to-do items.
var toDoItemsInDB = from Building todo in toDoDB.BuildingTable
select todo;
// Execute the query and place the results into a collection.
BuildingTable = new ObservableCollection<Building>(toDoItemsInDB);
Source = new CollectionViewSource();
Source.Source = BuildingTable;
// Call the base method.base.OnNavigatedTo(e);
}
For that purpose I added the lines:
public CollectionViewSource Source { get; set; }
Source = new CollectionViewSource();
Source.Source = BuildingTable;
I tried as well to put
Source = new CollectionViewSource();
Source.Source = BuildingTable;
in my MainPage Constructor. It doesnt work as well.
My Mainpage.xaml:
<!--<ListBox x:Name="toDoItemsListBox" ItemsSource="{Binding BuildingTable}" Grid.Row="0" Margin="12, 0, 12, 0" Width="440" SelectionChanged="goToNavigation">-->
<ListBox x:Name="toDoItemsListBox" ItemsSource="{Binding Source.View}" Grid.Row="0" Margin="12, 0, 12, 0" Width="440" SelectionChanged="goToNavigation">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Stretch" Width="440">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Name="textBlockShortcut" Text="{Binding Shortcut}" Width="Auto" HorizontalAlignment="Left" Grid.Column="0" Margin="0,0,0,5" FontSize="36" />
<TextBlock Name="textBlockName" Text="{Binding BuildingName}" Width="Auto" HorizontalAlignment="Left" Grid.Column="1" Margin="0,0,0,5" FontSize="36" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The first commented line shows the old working listbox without CollectionViewSource. So what am I missing?
EDIT:
private void goToNavigation(object sender, RoutedEventArgs e)
{
// If selected index is -1 (no selection) do nothing
if (toDoItemsListBox.SelectedIndex == -1)
return;
// Navigate to the new page
PhoneApplicationService.Current.State["SelectedItem"] = toDoItemsListBox.SelectedItem;
NavigationService.Navigate(new Uri("/NavigationPage.xaml", UriKind.Relative));
// Reset selected index to -1 (no selection)
toDoItemsListBox.SelectedIndex = -1;
}
You would usually create and bind to a CollectionViewSource in XAML:
<UserControl.Resources>
<CollectionViewSource x:Key="cvs"/>
</UserControl.Resources>
<Grid>
<ListBox ItemsSource="{Binding Source={StaticResource cvs}}" ...>
...
</ListBox>
</Grid>
and in code-behind just access that CollectionViewSource like this:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
...
var cvs = Resources["cvs"] as CollectionViewSource;
cvs.Source = BuildingTable;
}
You don't use the CollectionViewSource class directly, you use a CollectionView of the appropriate type.
View = CollectionViewSource.GetDefaultView( myCollection );
and then you bind that directly to your source.
ItemsSource="{Binding View}"
You can and only should use a CollectionViewSource from xaml, because thats its main purpose. From code you should directly create a CollectionView or use the GetDefaultView method.

When is ValidationSummary loaded?

I'm trying to realize when the ValidationSummary really loads and how can I force it to be loaded.
I've suscribed to loaded event to force a page validation and this is only triggered when I cause any "new" validation or open a ComboBox or something like this.
Any idea?? Thanks in advance.
Here goes my view:
<Grid Margin="0,3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"></ColumnDefinition>
<ColumnDefinition Width="60" />
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="60"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<sdk:Label Grid.Column="0" Target="{Binding ElementName=txtImporteTotal}" Content="Total Acto" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="5,0"></sdk:Label>
<TextBox Grid.Column="1" Name="txtImporteTotal" Margin="5,0" VerticalAlignment="Center" HorizontalAlignment="Stretch" Text="{Binding ActoMedico.importe_total, Mode=TwoWay, ValidatesOnExceptions=True, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"></TextBox>
<sdk:DescriptionViewer Target="{Binding ElementName=txtImporteTotal}" Grid.Column="2"></sdk:DescriptionViewer>
<sdk:Label Grid.Column="3" Target="{Binding ElementName=txtImporteMedico}" Content="Total Médico" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="5,0"></sdk:Label>
<TextBox Grid.Column="4" Name="txtImporteMedico" Margin="5,0" VerticalAlignment="Center" HorizontalAlignment="Stretch" Text="{Binding ActoMedico.importe_medico, Mode=TwoWay, ValidatesOnExceptions=True, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"></TextBox>
<sdk:DescriptionViewer Target="{Binding ElementName=txtImporteMedico}" Grid.Column="5"></sdk:DescriptionViewer>
This is its code behind, where I'm forcing the validation:
public ActoMedico()
{
InitializeComponent();
this.validationSummary.Loaded += new RoutedEventHandler(validationSummary_Loaded);
}
void validationSummary_Loaded(object sender, RoutedEventArgs e)
{
this.forzarValidacion();
}
private void forzarValidacion()
{
this.txtImporteMedico.GetBindingExpression(TextBox.TextProperty).UpdateSource();
this.txtImporteTotal.GetBindingExpression(TextBox.TextProperty).UpdateSource();
}
And finally, this is the model:
#region importe_medico
public const string importe_medicoPropertyName = "importe_medico";
private double? _importe_medico;
[Display(Description = "Importe")]
[Required(ErrorMessage = "Debe indicar el importe")]
public double? importe_medico
{
get
{
return _importe_medico;
}
set
{
Validator.ValidateProperty(value, new ValidationContext(this, null, null) { MemberName = importe_medicoPropertyName });
_importe_medico = value;
RaisePropertyChanged(importe_medicoPropertyName);
}
}
#endregion
#region importe_total
public const string importe_totalPropertyName = "importe_total";
private double? _importe_total;
[Display(Description = "Importe total")]
[Required(ErrorMessage = "Debe indicar el importe total")]
public double? importe_total
{
get
{
return _importe_total;
}
set
{
Validator.ValidateProperty(value, new ValidationContext(this, null, null) { MemberName = importe_totalPropertyName });
_importe_total = value;
RaisePropertyChanged(importe_totalPropertyName);
}
}
#endregion
As far as I see, you use the binding like {Binding ActoMedico.importe_total} in your text boxes. It means that the object bound to the UserControl must contain the property ActoMedico and the value of this property must contain the inner property importe_total.
I've added the following code to the code behind:
public class MainViewModel
{
public MainViewModel()
{
this.ActoMedico = new ItemViewModel();
}
public ItemViewModel ActoMedico { get; set; }
}
//...
public ActoMedico()
{
InitializeComponent();
this.DataContext = new MainViewModel(); //should be set in order to make bindings work
this.validationSummary.Loaded += new RoutedEventHandler(validationSummary_Loaded);
}
In the code above the ItemViewModel class is the class which contains those two properties from you last block of code. This class has a different name in your application, but I don't know it so I've named it in my way.
So now the ValidationSummary control will be displayed but with messages like Input string had an incorrect format. It is because the TextBox control expects the string data type whereas your view model has the double? data type. You can write a converter as a workaround to this issue:
public class DoubleToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var str = (string)value;
double d;
if (!double.TryParse(str, out d))
return null;
return d;
}
}
Usage:
<UserControl.Resources>
<local:DoubleToStringConverter x:Key="DoubleToStringConverter" />
</UserControl.Resources>
<!-- ... -->
<TextBox Text="{Binding ActoMedico.importe_total, Mode=TwoWay, Converter={StaticResource DoubleToStringConverter} ...
Anyway data annotations aren't easy to work with, so I reccomend to use the MVVM pattern and the INotifyDataErrorInfo interface. I've implemented the example of such validation here in my post, you can download source code and look how it is implemented.
Finally I get this by forcing a combobox to drop-down, force all the form validations and then undo the drop-down.
So I get the ValidationSummary from the beginning (I want every error from the start to be shown and disable the "save" button for the user.
Thanks so much for your help #vorrtex.

Dependency Property WPF Always Returning NULL

I have a UserControl called SharpComboBox. I am using MVVM model to populate the SharpComboBox with Categories. For that I need to set the ItemsSource property. Here is the usage of the SharpComboBox control.
<sharpControls:SharpComboBox ItemsSource="{Binding Path=Categories}" Grid.Column="1" Grid.Row="1" DisplayMemberPath="Title">
</sharpControls:SharpComboBox>
The Window is called the AddBook.xaml and here is the code behind:
public AddBooks()
{
InitializeComponent();
this.DataContext = new AddBookViewModel();
}
And here is the implementation of the AddBookViewModel.
public class AddBookViewModel
{
private CategoryRepository _categoryRepository;
public AddBookViewModel()
{
_categoryRepository = new CategoryRepository();
}
public List<Category> Categories
{
get
{
return _categoryRepository.GetAll();
}
}
And finally here is the SharpComboBox control:
<StackPanel Name="stackPanel">
<ComboBox x:Name="comboBox">
<ComboBox.ItemTemplate>
<DataTemplate>
<ItemsControl>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="2*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
</Grid>
<TextBlock Grid.Column="0" Grid.Row="0" Text="{Binding Path=Title}" Margin="10" />
<Image Grid.Column="1" Margin="10" Grid.Row="0" Width="100" Height="100" Stretch="Fill" Source="{Binding Path=ImageUrl}">
</Image>
</ItemsControl>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
And here is the code behind:
public partial class SharpComboBox : UserControl
{
public static DependencyProperty ItemsSourceProperty;
public SharpComboBox()
{
InitializeComponent();
this.DataContextChanged += new System.Windows.DependencyPropertyChangedEventHandler(SharpComboBox_DataContextChanged);
ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof (IEnumerable),
typeof (SharpComboBox), null);
comboBox.ItemsSource = ItemsSource;
}
public IEnumerable ItemsSource
{
get { return (IEnumerable) GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
void SharpComboBox_DataContextChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
{
}
For some reason the ItemsSource property is always null.
UPDATED:
void SharpComboBox_DataContextChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
{
var binding = new Binding();
binding.Source = this.DataContext;
**binding.Path = new PropertyPath("Categories");**
comboBox.SetBinding(ComboBox.ItemsSourceProperty, binding);
}
kek444 is very close, but there is one critical element missing. I've noticed your ViewModel doesn't implement INotifyPropertyChanged. This will prevent bindings from automatically refreshing when you've set that property. So, as kek444 mentioned, you are intially binding to null (because it's early) and then when you set it, you aren't informing your View of the change. It's pretty simple to change, though.
public class AddBookViewModel : INotifyPropertyChanged
{
event PropertyChangedEventHandler PropertyChanged;
public AddBookViewModel()
{
_categoryRepository = new CategoryRepository();
if(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Categories");
}
}
...
}
Anytime you change your backing store (CategoryRepository), you'll want to do this. There may be some additional complication here depending on your your repository is implemented, but this information ought to at least explain what is going on.
As a practice, I generally create a base ViewModel class that implements INotifyPropertyChanged so I can add a few helper methods that wrap that PropertyChanged logic up... I just call OnPropertyChanged("MyProp"); and that's it.
One other thing that might help you is that bindings will report to the debug output if you configure them correctly: Debugging WPF Binding
Hope this helps.
You cannot simply set the comboBox.ItemsSource from your property once in the constructor, who knows how early that happens. You need to set a binding between those two properties.

Resources