Force or otherwise initiate TwoWay TestBox data binding - c#

I've got a WPF TextBox with TwoWay binding to a ViewModel property. I also have a ToolBar with a Button. When the Button is clicked, it executes a command on the same ViewModel that will do something with the property the TextBox is bound to.
Unfortunately it looks like the Binding only sends the text back to the binding target when the TextBox loses focus. The Button on the Toolbar however does not take focus when clicked. The upshot being that when the Command executes it does not have the text from the textbox, but rather the last value that was bound.
The Xaml looks like so:
<DockPanel LastChildFill="True" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" >
<ToolBarTray Background="White" DockPanel.Dock="Top">
<ToolBar Band="1" BandIndex="1">
<Button Command="{Binding QueryCommand}">
<Image Source="images\media_play_green.png" />
</Button>
</ToolBar>
</ToolBarTray>
<DataGrid VerticalAlignment="Top" DockPanel.Dock="Top" Height="450" AutoGenerateColumns="True"
ItemsSource="{Binding}" DataContext="{Binding Results}" DataContextChanged="DataGrid_DataContextChanged"/>
<TextBox DockPanel.Dock="Bottom" Text="{Binding Sql, Mode=TwoWay}"
AcceptsReturn="True" AcceptsTab="True" AutoWordSelection="True" TextWrapping="WrapWithOverflow"/>
</DockPanel>
How do I get the TextBox's Text binding to update the ViewModel when the ToolBar button is pressed. There is nothing fancy going on in the ViewModel which looks like so:
public class MainViewModel : ViewModelBase
{
private readonly IMusicDatabase _database;
/// <summary>
/// Initializes a new instance of the MainViewModel class.
/// </summary>
public MainViewModel(IMusicDatabase database)
{
_database = database;
QueryCommand = new RelayCommand(Query);
}
public RelayCommand QueryCommand { get; private set; }
private async Task QueryAndSetResults()
{
Results = await _database.Query(Sql);
}
private void Query()
{
QueryAndSetResults();
}
private IEnumerable<object> _results;
public IEnumerable<object> Results
{
get
{
return _results;
}
private set
{
Set<IEnumerable<object>>("Results", ref _results, value);
}
}
private string _sql = "SELECT * FROM this WHERE JoinedComposers = 'Traditional'";
public string Sql
{
get { return _sql; }
set
{
Set<string>("Sql", ref _sql, value);
}
}
}

You can use the UpdateSourceTrigger property of the binding, setting it to PropertyChanged makes the TextBox refresh the binding every time the text changes, not just when losing focus:
<TextBox DockPanel.Dock="Bottom"
Text="{Binding Sql, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
AcceptsReturn="True"
AcceptsTab="True"
AutoWordSelection="True"
TextWrapping="WrapWithOverflow"/>
More info at MSDN.

Related

How to bind a property both ways on a control inside a DataGridTemplateColumn?

I have an ObservableCollection of DataPoint objects. That class has a Data property. I have a DataGrid. I have a custom UserControl called NumberBox, which is a wrapper for a TextBox but for numbers, with a Value property that is bind-able (or at least is intended to be).
I want the DataGrid to display my Data in a column NumberBox, so the value can be displayed and changed. I've used a DataGridTemplateColumn, bound the Value property to Data, set the ItemsSource to my ObservableCollection.
When the underlying Data is added or modified, the NumberBox updates just fine. However, when I input a value in the box, the Data doesn't update.
I've found answers suggesting to implement INotifyPropertyChanged. Firstly, not sure on what I should implement it. Secondly, I tried to implement it thusly on both my DataPoint and my NumberBox. I've found answers suggesting to add Mode=TwoWay, UpdateSourceTrigger=PropertyChange to my Value binding. I've tried both of these, separately, and together. Obviously, the problem remains.
Below is the bare minimum project I'm currently using to try to make this thing work. What am I missing?
MainWindow.xaml
<Window xmlns:BindingTest="clr-namespace:BindingTest" x:Class="BindingTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DataGrid Name="Container" AutoGenerateColumns="False" CanUserSortColumns="False" CanUserResizeColumns="False" CanUserResizeRows="False" CanUserReorderColumns="False" CanUserAddRows="False" CanUserDeleteRows="True">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Sample Text" Width="100" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<BindingTest:NumberBox Value="{Binding Data}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Button Name="BTN" Click="Button_Click" Height="30" VerticalAlignment="Bottom" Content="Check"/>
</Grid>
</Window>
MainWindow.cs
public partial class MainWindow : Window
{
private ObservableCollection<DataPoint> Liste { get; set; }
public MainWindow()
{
InitializeComponent();
Liste = new ObservableCollection<DataPoint>();
Container.ItemsSource = Liste;
DataPoint dp1 = new DataPoint(); dp1.Data = 1;
DataPoint dp2 = new DataPoint(); dp2.Data = 2;
Liste.Add(dp1);
Liste.Add(dp2);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
BTN.Content = Liste[0].Data;
}
}
DataPoint.cs
public class DataPoint
{
public double Data { get; set; }
}
NumberBox.xaml
<UserControl x:Class="BindingTest.NumberBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="28" d:DesignWidth="200">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Name="Container" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" VerticalContentAlignment="Center"/>
</Grid>
</UserControl>
NumberBox.cs
public partial class NumberBox : UserControl
{
public event EventHandler ValueChanged;
public NumberBox()
{
InitializeComponent();
}
private double _value;
public double Value
{
get { return _value; }
set
{
_value = value;
Container.Text = value.ToString(CultureInfo.InvariantCulture);
if (ValueChanged != null) ValueChanged(this, new EventArgs());
}
}
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
"Value",
typeof(double),
typeof(NumberBox),
new PropertyMetadata(OnValuePropertyChanged)
);
public static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
double? val = e.NewValue as double?;
(d as NumberBox).Value = val.Value;
}
}
What am I missing?
Quite a few things actually.
The CLR property of a dependency property should only get and set the value of the dependency property:
public partial class NumberBox : UserControl
{
public NumberBox()
{
InitializeComponent();
}
public double Value
{
get => (double)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
"Value",
typeof(double),
typeof(NumberBox),
new PropertyMetadata(0.0)
);
private void Container_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
Value = double.Parse(Container.Text, CultureInfo.InvariantCulture);
}
}
The TextBox in the control should bind to the Value property:
<TextBox Name="Container" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" VerticalContentAlignment="Center"
Text="{Binding Value, RelativeSource={RelativeSource AncestorType=UserControl}}"
PreviewTextInput="Container_PreviewTextInput"/>
The mode of the binding between the Value and the Data property should be set to TwoWay and also, and this is because the input control is in the CellTemplate of the DataGrid, the UpdateSourceTrigger should be set to PropertyChanged:
<local:NumberBox x:Name="nb" Value="{Binding Data, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
mm8's answer pointed me in the right direction.
The two key missing elements were indeed:
to change the TextBox in NumberBox.xaml such as
<TextBox Name="Container"
Text="{Binding Value, RelativeSource={RelativeSource AncestorType=UserControl}}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" VerticalContentAlignment="Center"
/>
and to change the binding in MainWindow.xaml such as
...
<DataTemplate>
<BindingTest:NumberBox
Value="{Binding Data, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
...
With these two modifications, both the Value and Data are being updated two-ways inside my DataGridTemplaceColumn.
Binding to the text however proved problematic. WPF apparently uses en-US culture no matter how your system is setup, which is really bad as this control deals with numbers. Using this trick solves that problem, and now the correct decimal separator is recognised. In my case, I've added it in a static constructor for the same effect so NumberBox can be used in other applications as is.
static NumberBox()
{
FrameworkElement.LanguageProperty.OverrideMetadata(
typeof(FrameworkElement),
new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)));
}
I've tested this in my test project, and then again tested the integration into my real project. As far as I can tell, it holds water, so I'll consider this question answered.

WPF .NET unable to add new instance to ObservableCollection

I am new to WPF and losing my mind with issues. I have a view, viewmodel and model. I want the user user to fill in some information in the view, press button to confirm and then have a new instance of the model (with the user specified parameters) added to the ObservableCollection and to my local database.
View: (unrelated stuff hidden)
<TextBox DataContext="{DynamicResource RiderequestViewModel}" Margin="15,0,15,0" Grid.Column="1" Grid.Row="1" FontSize="12" Height="25" Text="{Binding Riderequest.Time}"/>
<TextBox DataContext="{DynamicResource RiderequestViewModel}" Margin="15,0,15,0" Grid.Column="1" Grid.Row="2" FontSize="12" Height="25" Text="{Binding Riderequest.LocationFrom}"/>
<TextBox DataContext="{DynamicResource RiderequestViewModel}" Margin="15,0,15,0" Grid.Column="1" Grid.Row="3" FontSize="12" Height="25" Text="{Binding Riderequest.LocationTo}"/>
<Button DataContext="{DynamicResource RiderequestViewModel}" x:Name="nextBtn" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="5" Content="Verder" Width="150" Foreground="White" Command="{Binding AddRiderequestCommand}" Click="NextBtn_Click"/>
ViewModel RiderequestViewModel:
namespace Drink_n_Drive.ViewModel
{
class RiderequestViewModel: BaseViewModel
{
private Riderequest riderequest;
private ObservableCollection<Riderequest> riderequests;
public ObservableCollection<Riderequest> Riderequests
{
get
{
return riderequests;
}
set
{
riderequests= value;
NotifyPropertyChanged();
}
}
public Riderequest Riderequest
{
get
{
return riderequest;
}
set
{
riderequest= value;
NotifyPropertyChanged();
}
}
public ICommand AddRiderequestCommand { get; set; }
public ICommand ChangeRiderequestCommand { get; set; }
public ICommand DeleteRiderequestCommand { get; set; }
public RiderequestViewModel()
{
LoadRiderequests(); //load existing from DB
LinkCommands(); //Link ICommands with BaseCommands
}
private void LoadRiderequests()
{
RiderequestDataService riderequestDS = new RiderequestDataService();
Riderequests= new ObservableCollection<Riderequests>(riderequestDS .GetRiderequests());
}
private void LinkCommands()
{
AddRiderequestCommand = new BaseCommand(Add);
ChangeRiderequestCommand = new BaseCommand(Update);
DeleteRiderequestCommand = new BaseCommand(Delete);
}
private void Add()
{
RiderequestDataService riderequestDS = new RitaanvraagDataService();
riderequestDS.InsertRiderequest(riderequest); //add single (new) instance to the DB
LoadRiderequests(); //Reload ObservableCollection from DB
}
private void Update()
{
if (SelectedItem != null)
{
RiderequestDataService riderequestDS = new RiderequestDataService();
riderequestDS.UpdateRiderequest(SelectedItem);
LoadRiderequests(); //refresh
}
}
private void Delete()
{
if (SelectedItem != null)
{
RiderequestDataService riderequestDS = new RiderequestDataService();
riderequestDS.DeleteRiderequest(SelectedItem);
LoadRiderequests();
}
}
private Riderequest selectedItem;
public Riderequest SelectedItem
{
get { return selectedItem; }
set
{
selectedItem = value;
NotifyPropertyChanged();
}
}
}
}
Pressing the button simply does nothing and I don't know why. I also have a diffrent page where I want to show a datagrid of all instances in the ObservableCollection like this:
<DataGrid Grid.Row="1" Grid.ColumnSpan="2" Grid.RowSpan="3" DataContext="{DynamicResource RitaanvragenViewModel}" ItemsSource="{Binding Ritaanvragen}" SelectedItem="{Binding SelectedItem}" />
But the grid just shows completly empty. I have added some dummydata to my DB but still doesn't work.
My appologies for the mix of English and Dutch in the code.
I'm not 100% sure about it but i would do something like this:
As for first step I would change the TextBox to look like this:
<TextBox DataContext="{DynamicResource Ritaanvraag}" Margin="15,0,15,0" Grid.Column="1" Grid.Row="1" FontSize="12" Height="25" Text="{Binding Path=Time, Mode=OneWayToSource}"/>
There's no need to pass your ViewModel to it as a DataSource because your View's first few meta-data related lines should already define what ViewModel does it belong to.
When you not specify the type of your binding, it will use a default binding type which depends on the current object. You're using a TextBox now so it will have a TwoWay binding by default.
If you only want to accept data from the user and you don't want to show the data if your model has any then you should use OneWayToSource. (Note: OneWay is a direction between source -> view.)
I would also remove the DataSource from your DataGrid because you already set it's ItemSource:
<DataGrid Grid.Row="1" Grid.ColumnSpan="2" Grid.RowSpan="3" ItemsSource="{Binding Ritaanvragen}" SelectedItem="{Binding SelectedItem}" />

Context Menu Keep Getting Wrong Object using Mvvm

I am making a winodws 8 phone application and trying to have a context menu on it from the Windows Phone tool kit.
I been following this tutorial but instead of list box I am using a long list selector that is built into WP8
<DataTemplate x:Key="GroceryListItemTemplate">
<StackPanel Grid.Column="1" Grid.Row="1">
<TextBlock x:Name="tbName" TextWrapping="Wrap" Text="{Binding Name}" FontSize="32"/>
<TextBlock x:Name="tbProductInfo" TextWrapping="Wrap" Text="{Binding ProductInfoLabel}" HorizontalAlignment="Left"/>
</StackPanel>
<toolkit:ContextMenuService.ContextMenu>
<toolkit:ContextMenu>
<toolkit:MenuItem Header="Edit"
Command="{Binding GroceryItemsVm.EditGroceryItemCmd, Source={StaticResource Locator}}"
CommandParameter="{Binding}"/>
<toolkit:MenuItem Header="Delete" Command="{Binding GroceryItemsVm.DeleteGroceryItemCmd, Source={StaticResource Locator}}"
CommandParameter="{Binding}"/>
</toolkit:ContextMenu>
</toolkit:ContextMenuService.ContextMenu>
</DataTemplate>
above is a stripped down of what my code looks like
here is my list selector
<phone:LongListSelector IsGroupingEnabled="True" ItemsSource="{Binding GroceryItems}" HideEmptyGroups="True" LayoutMode="List" Grid.Row="1">
<phone:LongListSelector.ItemTemplate>
<StaticResource ResourceKey="GroceryListItemTemplate"/>
</phone:LongListSelector.ItemTemplate>
</phone:LongListSelector>
Here is my mvvm code I have
public class GroceryItemsVm : ViewModelBase
{
public GroceryItemsVm()
{
if (IsInDesignMode)
{
}
else
{
EditGroceryItemCmd = new RelayCommand<GroceryItem>(this.Edit);
DeleteGroceryItemCmd = new RelayCommand<GroceryItem>(this.Delete);
GroceryItems = // method that gets all items back as grouped.
}
}
private List<Group<GroceryItem>> groceryItems = null;
/// <summary>
/// Sets and gets the GroceryItems property.
/// Changes to that property's value raise the PropertyChanged event.
/// </summary>
public List<Group<GroceryItem>> GroceryItems
{
get
{
return groceryItems;
}
set
{
if (groceryItems == value)
{
return;
}
RaisePropertyChanging(() => GroceryItems);
groceryItems = value;
RaisePropertyChanged(() => GroceryItems);
}
}
private async void Delete(GroceryItem obj)
{
// trigged on context delete
}
private void Edit(GroceryItem obj)
{
// triggered on context edit
}
public RelayCommand<GroceryItem> EditGroceryItemCmd
{
get;
private set;
}
public RelayCommand<GroceryItem> DeleteGroceryItemCmd
{
get;
private set;
}
}
public class GroceryItem : ObservableObject
{
/// <summary>
/// The <see cref="Name" /> property's name.
/// </summary>
public const string NamePropertyName = "Name";
private string name = "";
/// <summary>
/// Sets and gets the Name property.
/// Changes to that property's value raise the PropertyChanged event.
/// </summary>
public string Name
{
get
{
return name;
}
set
{
if (name == value)
{
return;
}
RaisePropertyChanging(() => Name);
name = value;
RaisePropertyChanged(() => Name);
}
}
}
Now when I run it, It works for the first time, whatever item I choose to edit it, will get the right object for it. However the next object will always be the same. It never changes it choice after the selection is done.
Edit
Here is an example.
https://onedrive.live.com/redir?resid=FAE864D71B4770C6!19080&authkey=!ACUC2xXmZLVD7fE&ithint=file%2c.zip
Run it
Trigger Context Menu to show over "1"
Hit Edit - note dialog message (will say 1)
Hit "Go back button"
Trigger Context Menu to show over "3"
Hit Edit - note dialog message (will say 3)
The only thing I can think of is override the back button for the pages I am going to and just do a Navigate to the page. It is kinda stupid but that's all I can think off.
public partial class MvvmView1 : PhoneApplicationPage
{
// Constructor
public MvvmView1()
{
InitializeComponent();
// Sample code to localize the ApplicationBar
//BuildLocalizedApplicationBar();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
NavigationService.GoBack();
}
protected override void OnBackKeyPress(CancelEventArgs e)
{
NavigationService.Navigate(new Uri("/MainPage.xaml", UriKind.Relative));
}
}
This is common problem with ContextMenu. I have been trying for some time to think of a solution searching all around for something. You said after you click once it nevers gets it right.
Try the following:
Add the unloaded handler to you contextmenu as follows:
<DataTemplate x:Key="GroceryListItemTemplate">
<StackPanel Grid.Column="1" Grid.Row="1">
<TextBlock x:Name="tbName" TextWrapping="Wrap" Text="{Binding Name}" FontSize="32"/>
<TextBlock x:Name="tbProductInfo" TextWrapping="Wrap" Text="{Binding ProductInfoLabel}" HorizontalAlignment="Left"/>
</StackPanel>
<toolkit:ContextMenuService.ContextMenu>
<toolkit:ContextMenu ***Unloaded="ContextMenu_Unloaded"***>
<toolkit:MenuItem Header="Edit"
Command="{Binding GroceryItemsVm.EditGroceryItemCmd, Source={StaticResource Locator}}"
CommandParameter="{Binding}"/>
<toolkit:MenuItem Header="Delete" Command="{Binding GroceryItemsVm.DeleteGroceryItemCmd, Source={StaticResource Locator}}"
CommandParameter="{Binding}"/>
</toolkit:ContextMenu>
</toolkit:ContextMenuService.ContextMenu>
</DataTemplate>
Remove the * I added them to emphasize the changes.
And then the code behind for that handler would be:
private void ContextMenu_Unloaded(object sender, RoutedEventArgs e)
{
var conmen = (sender as ContextMenu);
if (conmen != null)
conmen.ClearValue(DataContextProperty);
}
Let me know if this works.
Based on the comments, you have a GroceryItemsVm class that looks something like the following.
public class GroceryItemVm : INotifyPropertyChanged
{
public string Name { get; set; }
public string ProductInfoLabel{ get; set; }
public ICommand EditGroceryItemCmd { get; private set; }
public ICommand DeleteGroceryItemCmd { get; private set; }
}
And so the GroceryItems property that your LLS is binding to would be
public IEnumerable<GroceryItemVm> GroceryItems { get; set;}
If this is the case, then the DataContext of the items within your DataTemplate is an instance of GroceryItemsVm. All of your bindings within the DataTemplate should bind directly to that instance
<DataTemplate x:Key="GroceryListItemTemplate">
<StackPanel Grid.Column="1" Grid.Row="1">
<TextBlock x:Name="tbName" TextWrapping="Wrap" Text="{Binding Name}" FontSize="32"/>
<TextBlock x:Name="tbProductInfo" TextWrapping="Wrap" Text="{Binding ProductInfoLabel}" HorizontalAlignment="Left"/>
</StackPanel>
<toolkit:ContextMenuService.ContextMenu>
<toolkit:ContextMenu>
<toolkit:MenuItem Header="Edit" Command="{Binding EditGroceryItemCmd}"/>
<toolkit:MenuItem Header="Delete" Command="{Binding DeleteGroceryItemCmd}"/>
</toolkit:ContextMenu>
</toolkit:ContextMenuService.ContextMenu>
</DataTemplate>

Calling event in UserControl from MainWindow

I have a UserControl with a Button and a ListView.
Model
public class Item
{
private string _name = string.Empty;
public string Name
{
get
{
return _name;
}
set
{
_name = value;
}
}
}
ViewModel
public class ViewModel : NotifyProperty
{
private Command addCommand;
public ICommand AddCommand
{
get
{
if (addCommand == null)
addCommand = new Command(addItem);
return addCommand;
}
}
private ObservableCollection<Item> _itemCollection;
public ViewModel()
{
ItemCollection = new ObservableCollection<Item>();
Item newItem = new Item();
newItem.Name = "Joe";
ItemCollection.Add(newItem);
}
public ObservableCollection<Item> ItemCollection
{
get
{
return _itemCollection;
}
set
{
_itemCollection = value;
OnPropertyChanged("ItemCollection");
}
}
private void addItem(Object obj)
{
Item newItem = new Item();
newItem.Name = "Chris";
ItemCollection.Add(newItem);
}
}
UserControl (XAML)
<UserControl.DataContext>
<local:ViewModel />
</UserControl.DataContext>
<UserControl.Resources>
<DataTemplate x:Key="ItemTemplate">
<StackPanel Orientation="Vertical">
<Label Content="{Binding Name}" />
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<DockPanel>
<Button Width="100" Height="30" Content="Add" Command="{Binding AddCommand}" DockPanel.Dock="Top" />
<ListView ItemTemplate="{StaticResource ItemTemplate}" ItemsSource="{Binding ItemCollection}" />
</DockPanel>
</Grid>
I then add this to my MainWindow like so
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.mainContentControl.Content = new ListControl();
}
}
This works fine and when I click the "Add" button the name "Chris" gets added to the ListView.
Now I add a button to MainView and bind its Command property to my ViewModel like so:
<Grid>
<DockPanel>
<Button Width="100" Height="30" Content="Add" Command="{Binding AddCommand}" DockPanel.Dock="Top">
<Button.DataContext>
<local:ViewModel />
</Button.DataContext>
</Button>
<ContentControl x:Name="mainContentControl" />
</DockPanel>
</Grid>
When I click this button in the MainWindow the command is sent to the ViewModel, the addItem event gets called, the name "Chris" gets added to the ItemCollection, but the ListView doesn't update. What am I doing wrong?
Is your ViewModel being set as the data context of another element somewhere else (either in XAML or code-behind).
Where you're setting it as the data context against the button, that will instantiate a new instance of the view model, so any interaction with the instance the button has access to will not update across other instances.
The button will inherit the data context from ancestor elements (e.g. the window etc), so you shouldn't need to set it, but if you do need a separate data context for the button, then I'd recommend creating the instance of ViewModel as a resource and then just referencing that for the elements that need access to it.

Dynamic user control change - WPF

I'm developing an app in WPF and I need to change in runtime a content of a ContentControl depending than the user selected on ComboBox.
I have two UserControls and at my combo exists two itens, corresponding each one each.
First usercontrol:
<UserControl x:Class="Validator.RespView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="167" d:DesignWidth="366" Name="Resp">
<Grid>
<CheckBox Content="CheckBox" Height="16" HorizontalAlignment="Left" Margin="12,12,0,0" Name="checkBox1" VerticalAlignment="Top" />
<ListBox Height="112" HorizontalAlignment="Left" Margin="12,43,0,0" Name="listBox1" VerticalAlignment="Top" Width="168" />
<Calendar Height="170" HorizontalAlignment="Left" Margin="186,0,0,0" Name="calendar1" VerticalAlignment="Top" Width="180" />
</Grid>
Second usercontrol:
<UserControl x:Class="Validator.DownloadView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="76" d:DesignWidth="354" Name="Download">
<Grid>
<Label Content="States" Height="28" HorizontalAlignment="Left" Margin="12,12,0,0" Name="label1" VerticalAlignment="Top" />
<ComboBox Height="23" HorizontalAlignment="Left" Margin="12,35,0,0" Name="comboBox1" VerticalAlignment="Top" Width="120" />
<RadioButton Content="Last 48 hs" Height="16" HorizontalAlignment="Left" Margin="230,42,0,0" Name="rdbLast48" VerticalAlignment="Top" />
<Label Content="Kind:" Height="28" HorizontalAlignment="Left" Margin="164,12,0,0" Name="label2" VerticalAlignment="Top" />
<RadioButton Content="General" Height="16" HorizontalAlignment="Left" Margin="165,42,0,0" Name="rdbGeral" VerticalAlignment="Top" />
</Grid>
At MainWindowView.xaml
<Window x:Class="Validator.MainWindowView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:du="clr-namespace:Validator.Download"
xmlns:resp="clr-namespace:Validator.Resp"
Title="Validator" Height="452" Width="668"
WindowStartupLocation="CenterScreen" ResizeMode="NoResize">
<Window.Resources>
<DataTemplate DataType="{x:Type du:DownloadViewModel}">
<du:DownloadView/>
</DataTemplate>
<DataTemplate DataType="{x:Type resp:RespViewModel}">
<resp:RespView/>
</DataTemplate>
</Window.Resources>
<Grid>
<ComboBox ItemsSource="{Binding Path=PagesName}"
SelectedValue="{Binding Path=CurrentPageName}"
HorizontalAlignment="Left" Margin="251,93,0,0"
Name="cmbType"
Width="187" VerticalAlignment="Top" Height="22"
SelectionChanged="cmbType_SelectionChanged_1" />
<ContentControl Content="{Binding CurrentPageViewModel}" Height="171" HorizontalAlignment="Left" Margin="251,121,0,0" Name="contentControl1" VerticalAlignment="Top" Width="383" />
</Grid>
</Window>
I assigned to the DataContext of the MainView, the viewmodel below:
public class MainWindowViewModel : ObservableObject
{
#region Fields
private ICommand _changePageCommand;
private ViewModelBase _currentPageViewModel;
private ObservableCollection<ViewModelBase> _pagesViewModel = new ObservableCollection<ViewModelBase>();
private readonly ObservableCollection<string> _pagesName = new ObservableCollection<string>();
private string _currentPageName = "";
#endregion
public MainWindowViewModel()
{
this.LoadUserControls();
_pagesName.Add("Download");
_pagesName.Add("Resp");
}
private void LoadUserControls()
{
Type type = this.GetType();
Assembly assembly = type.Assembly;
UserControl reso = (UserControl)assembly.CreateInstance("Validator.RespView");
UserControl download = (UserControl)assembly.CreateInstance("Validator.DownloadView");
_pagesViewModel.Add(new DownloadViewModel());
_pagesViewModel.Add(new RespViewModel());
}
#region Properties / Commands
public ICommand ChangePageCommand
{
get
{
if (_changePageCommand == null)
{
_changePageCommand = new RelayCommand(
p => ChangeViewModel((IPageViewModel)p),
p => p is IPageViewModel);
}
return _changePageCommand;
}
}
public ObservableCollection<string> PagesName
{
get { return _pagesName; }
}
public string CurrentPageName
{
get
{
return _currentPageName;
}
set
{
if (_currentPageName != value)
{
_currentPageName = value;
OnPropertyChanged("CurrentPageName");
}
}
}
public ViewModelBase CurrentPageViewModel
{
get
{
return _currentPageViewModel;
}
set
{
if (_currentPageViewModel != value)
{
_currentPageViewModel = value;
OnPropertyChanged("CurrentPageViewModel");
}
}
}
#endregion
#region Methods
private void ChangeViewModel(IPageViewModel viewModel)
{
int indexCurrentView = _pagesViewModel.IndexOf(CurrentPageViewModel);
indexCurrentView = (indexCurrentView == (_pagesViewModel.Count - 1)) ? 0 : indexCurrentView + 1;
CurrentPageViewModel = _pagesViewModel[indexCurrentView];
}
#endregion
}
On MainWindowView.xaml.cs, I wrote this event to do the effective change:
private void cmbType_SelectionChanged_1(object sender, SelectionChangedEventArgs e)
{
MainWindowViewModel element = this.DataContext as MainWindowViewModel;
if (element != null)
{
ICommand command = element.ChangePageCommand;
command.Execute(null);
}
}
The app run ok and I inspected the application with WPFInspector and saw that the view changes when the combobox is changed internally, but the ContentControl still empty visually..
Sorry about the amount of code that I posted and my miss of knowledge but I'm working with this a long time and can't solve this problem.
Thanks
Issues:
Firstly don't ever create View related stuff in the ViewModel (UserControl). This is no longer MVVM when you do that.
Derive ViewModels from ViewModelBase and not ObservableObject unless you have a compelling reason to not use ViewModelBase when using MVVMLight. Keep ObservableObject inheritence for Models. Serves as a nice separation between VM's and M's
Next you do not need to make everything an ObservableCollection<T> like your _pagesViewModel. You do not have that bound to anything in your View's so it's just a waste. Just keep that as a private List or array. Check what a type actually does in difference to a similar other one.
Not sure about this one, maybe you pulled this code snippet as a demo, but do not use margins to separate items in a Grid. Your Layout is essentially just 1 Grid cell and the margins have the items not overlap. If you're not aware of that issue, Check into WPF Layout Articles.
Please don't forget principles of OOP, Encapsulation and sorts when writing a UI app. When having Properties like CurrentPageViewModel which you don't intend the View to switch make the property setter private to enforce that.
Don't resort to code-behind in the View too soon. Firstly check if it's only a View related concern before doing so. Am talking about your ComboBox SelectionChanged event handler. Your purpose of that in this demo is to switch the Bound ViewModel which is held in the VM. Hence it's not something that the View is solely responsible for. Thus look for a VM involved approach.
Solution:
You can get a working example of your code with the fixes for above from Here and try it out yourself.
Points 1 -> 5 are just basic straightforward changes.
For 6, I've created a SelectedVMIndex property in the MainViewModel which is bound to the SelectedIndex of the ComboBox. Thus when the selected index flips, the property setter after updating itself updates the CurrentPageViewModel as well such as
public int SelectedVMIndex {
get {
return _selectedVMIndex;
}
set {
if (_selectedVMIndex == value) {
return;
}
_selectedVMIndex = value;
RaisePropertyChanged(() => SelectedVMIndex);
CurrentPageViewModel = _pagesViewModel[_selectedVMIndex];
}
}

Categories