How to Get a Reference to a ViewModel - c#

All, I have a custom DataGridView control which overrides the DataGidView's OnItemsSourceChanged event. Inside this event I need to get a reference to a data set in the relevant ViewModel. Code is
public class ResourceDataGrid : DataGrid
{
protected override void OnItemsSourceChanged(
System.Collections.IEnumerable oldValue,
System.Collections.IEnumerable newValue)
{
if (Equals(newValue, oldValue))
return;
base.OnItemsSourceChanged(oldValue, newValue);
ResourceCore.ResourceManager manager = ResourceCore.ResourceManager.Instance();
ResourceDataViewModel resourceDataViewModel = ?? // How do I get my ResourceDataViewModel
List<string> l = manger.GetDataFor(resourceDataViewModel);
...
}
}
On the marked line I want to know how to get a reference to ResourceDataViewModel resourceDataViewModel. The reson is that i have multiple tabs each tab contains a data grid and ascociated ViewModel, the ViewModel holds some data that I need to retrieve [via the ResourceManager] (or is there another, better way?).
The question is, from the above event, how can I get the ascociated ResourceDataViewModel?
Thanks for your time.

Get the DataContext and cast it to the view-model type:
var viewModel = this.DataContext as ResourceDataViewModel

Put a static reference to it on your app, when the VM is created place its reference on the static and access it as needed.

You ask if there is a better way... In my experience if you find yourself subclassing a UI element in WPF there ususally is.
You can get away from embedding business logic (the choice of which data to display in the grid), by databinding your entire tab control to a view model.
To demonstrate - here is a very simple example. This is my XAML for the window hosting the tab control:
<Window x:Class="WpfApplication1.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>
<TabControl ItemsSource="{Binding Tabs}" SelectedItem="{Binding SelectedTab}">
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Header" Value="{Binding TabName}"></Setter>
</Style>
</TabControl.ItemContainerStyle>
<TabControl.ContentTemplate>
<DataTemplate>
<Grid>
<DataGrid ItemsSource="{Binding TabData}"></DataGrid>
</Grid>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
The data context of my window is a TabsViewModel (I am using the NotificationObject that can be found in the PRISM NuGet Package):
public class TabsViewModel: NotificationObject
{
public TabsViewModel()
{
Tabs = new[]
{
new TabViewModel("TAB1", "Data 1 Tab 1", "Data 2 Tab1"),
new TabViewModel("TAB2", "Data 1 Tab 2", "Data 2 Tab2"),
};
}
private TabViewModel _selectedTab;
public TabViewModel SelectedTab
{
get { return _selectedTab; }
set
{
if (Equals(value, _selectedTab)) return;
_selectedTab = value;
RaisePropertyChanged(() => SelectedTab);
}
}
public IEnumerable<TabViewModel> Tabs { get; set; }
}
public class TabViewModel
{
public TabViewModel(string tabName, params string[] data)
{
TabName = tabName;
TabData = data.Select(d => new RowData(){Property1 = d}).ToArray();
}
public string TabName { get; set; }
public RowData[] TabData { get; set; }
}
public class RowData
{
public string Property1 { get; set; }
}
This is obviously an over simplified case, but it means that if there is any business logic about precisely what data to show in each tab, this can reside in one of the view models, as opposed to the code behind. This gives you all the 'separation of concerns' benefits that MVVM is designed to encourage...

Related

How to access the main DataContext from within a template

Summary
I've got an element within a data template, that I want bound to some property of the main data context.
I realise that in this specific situation, a different solution may be preferable (and I have a working solution that avoids this), but I suspect this kind of problem may come up again and I want to know how to solve it in the general case.
Below are the specifics of my situation.
The Details
Data Hierarchy: I have a list of type A, each instance of A has a list of type B, each instance of B has some other data including a string for a text log.
UI Structure: I have a ComboBox to select an item of type A. I have a TabControl with the tabs representing items of type B, taken from the selected A above. In each tab, there is a means to enter data to populate the object of type B, and a log, representing changes to that instance of B.
Backing Logic: I track the selected item in each list with properties (SelectionA and SelectionB in the data context, MainWindowViewModel) that notify when they change. The B object also notifies when its log text changes. These ensure that the UI responds to changes to the backing data.
Problem: I want to move the notify logic to all be in one place (the DataContext, i.e. MainWindowViewModel), rather than having some in the B class and needing to duplicate the notify logic. To achieve this, I add a property (SelectionBLogText) to track the LogText property of the SelectionB object, and bind the log (in the templated tabpanel) to the main SelectionBLogText property. The problem is that within the tabpage, I can only seem to bind to properties of the selected B object (from the selected tab), and I need to bind to a property of the DataContext instead. I've tried using RelativeSource but nothing I've tried so far works, and the more I look at the docs the more I feel it's designed for another job.
The XAML (with irrelevant details removed):
<Window x:Class="WPFQuestion.MainWindow"
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:local="clr-namespace:WPFQuestion"
mc:Ignorable="d"
Title="MainWindow"
Height="350"
Width="930">
<DockPanel>
<ComboBox
ItemsSource="{Binding ListOfA}"
SelectedItem="{Binding SelectionA}"
DisplayMemberPath="Name"/>
<TabControl
ItemsSource="{Binding SelectionA}"
SelectedItem="{Binding SelectionB}"
DisplayMemberPath="Name">
<TabControl.ContentTemplate>
<ItemContainerTemplate>
<StackPanel>
<TextBox
IsReadOnly="True"
Text="{Binding Path=???.SelectionBLogText}"/>
<Button Click="ClearLogButton_Click"/>
</StackPanel>
</ItemContainerTemplate>
</TabControl.ContentTemplate>
</TabControl>
</DockPanel>
</Window>
And the code-behind:
public partial class MainWindow : Window
{
internal MainWindowViewModel vm;
public MainWindow()
{
InitializeComponent();
vm = new MainWindowViewModel();
DataContext = vm;
}
// Various methods for event handling
}
public class A : IEnumerable<B>
{
public string Name { get; set; }
public List<B> Bs { get; set; }
}
public class B // previously : INotifyPropertyChanged
{
public string Name { get; set; }
public string LogText { get; set; }
// various other properties
}
public class MainWindowViewModel : INotifyPropertyChanged
{
private A _a;
private B _b;
public event PropertyChangedEventHandler PropertyChanged;
public List<A> ListOfA { get; set; }
public A SelectionA
{
get => _a;
set
{
if (_a == value)
{
return;
}
_a = value;
RaisePropertyChanged(nameof(SelectionA));
}
}
public B SelectionB
{
get => _b;
set
{
if (_b == value)
{
return;
}
_b = value;
RaisePropertyChanged(nameof(SelectionB));
RaisePropertyChanged(nameof(SelectionBLogText));
}
}
public string SelectionBLogText
{
get => SelectionB.LogText;
set
{
if (SelectionB.LogText == value)
{
return;
}
SelectionB.LogText = value;
RaisePropertyChanged(nameof(SelectionBLogText));
}
}
private void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
have you tried something like this when you used relative binding? if not please check this out.
<TextBox IsReadOnly="True"
Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window},
Path=Datacontext.SelectionBLogText}"/>

WPF: TabControl with multiple DataTemplates

I have a TabControl with multiple DataTemplate. the first DataTemplate will be used for search reasons and the second will be for displaying items obtained from that search. My XAML code will be as follows:
<UserControl.Resources>
<!--First template-->
<DataTemplate>
<!-- I will have a DataGrid here-->
</DataTemplate>
<!--Second template-->
<DataTemplate >
<!-- I will have details of one item of the DataGrid-->
</DataTemplate>
</UserControl.Resources>
<TabControl ItemsSource="{Binding }"/>
What I want to accomplish is that in the TabControl the first tab will contain the first DataTemplate (the search template) and when I double click on one row of my DataGrid, a tab will be added with the details of that row (in other words a tab with the second template).
Since I am using MVVM, I thought of creating two UserControls, one for each template and then catch the double click event, but after this I don't know how to add a tab since now my search template is a UserControl seperated from the one that contains the TabControl.
So how do I do this?
UPDATE:
As I read the answers I think I wasn't very clear in stating the problem.
My problem is how to add tabs with the second template, by catching double click events from the first template. I don't have any problem in adding the two templates independently.
Rather can creating two UserControls, you can create and use a DataTemplateSelector in order to switch different DataTemplates in.
Basically, create a new class that inhereits from DataTemplateSelector and override the SelecteTemplate method. Then declare an instance of it in the XAML (much like a value converter), and then apply it to ContentTemplateSelector property of the TabControl.
More info can be found here.
If you're going to do this with MVVM, your tab control should be bound to some ObservableCollection in your VM, and you just add and remove VM's to the collection as needed.
The VMs can be any type you like and your DataTemplates will show the correct view in the tab just like any other view, so yes, create two UserControls for the two views.
public class MainVM
{
public ObservableCollection<object> Views { get; private set; }
public MainVM()
{
this.Views = new ObservableCollection<object>();
this.Views.Add(new SearchVM(GotResults));
}
private void GotResults(Results results)
{
this.Views.Add(new ResultVM(results));
}
}
There are two options: Use datatemplate selector, or use implicit datatemplates and different types for each tabitem.
1. DataTemplateSelector:
public ObservableCollection<TabItemVM> Tabs { get; private set; }
public MainVM()
{
Tabs = ObservableCollection<TabItemVM>
{
new TabItemVM { Name="Tab 1" },
};
}
void AddTab(){
var newTab = new TabItemVM { Name="Tab 2" };
Tabs.Add(newTab);
//SelectedTab = newTab; //you may bind TabControl.SelectedItemProperty to viewmodel in order to be able to activate the tab from viewmodel
}
public class TabItemTemplateSelector : DataTemplateSelector
{
public DataTemplate Tab1Template { get; set; }
public DataTemplate Tab2Template { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var tabItem = item as TabItemVM;
if (tabItem.Name == "Tab 1") return Tab1Template;
if (tabItem.Name == "Tab 2") return Tab2Template;
return base.SelectTemplate(item, container);
}
}
<local:TabItemTemplateSelector
x:Key="TabItemTemplateSelector"
Tab1Template="{StaticResource Tab1Template}"
Tab2Template="{StaticResource Tab2Template}" />
2. Implicit Data Templates:
public class MainVM : ViewModelBase
{
public ObservableCollection<TabItemVM> Tabs { get; private set; }
public MainVM()
{
Tabs = new ObservableCollection<TabItemVM>
{
new Tab1VM(),
};
}
void AddTab()
{
var newTab = new Tab2VM()
Tabs.Add(newTab);
//SelectedTab = newTab;
}
}
public class TabItemBase
{
public string Name { get; protected set; }
}
public class Tab1VM : TabItemBase
{
public Tab1VM()
{
Name = "Tab 1";
}
}
public class Tab2VM : TabItemBase
{
public Tab2VM()
{
Name = "Tab 2";
}
}
<UserControl.Resources>
<!--First template-->
<DataTemplate DataType="local:Tab1VM">
<!-- I will have a DataGrid here-->
</DataTemplate>
<!--Second template-->
<DataTemplate DataType="local:Tab2VM">
<!-- I will have details of one item of the DataGrid-->
</DataTemplate>
</UserControl.Resources>

Nested ObservableCollection data binding in WPF

I am very new to WPF and trying to create a self learning application using WPF.
I am struggling to understand concepts like data binding,data templates,ItemControls to a full extent.
I am trying to create a learn page with the following requirements in mind.
1) The page can have more than one question.A scroll bar should be displayed once the
questions fills up the whole page.
2) The format of the choices vary based on the question type.
3) the user shall be able to select the answer for the question.
I am facing problem with binding nested ObservableCollection and displaying the content as for the above requirements.
Can someone help in how to create a page as shown below and how to use INotifyPropertyChanged along XMAL to do the nested binding.
Here is the basic code I am trying to use to use display the questions and answers.
namespace Learn
{
public enum QuestionType
{
OppositeMeanings,
LinkWords
//todo
}
public class Question
{
public Question()
{
Choices = new ObservableCollection<Choice>();
}
public string Name { set; get; }
public string Instruction { set; get; }
public string Clue { set; get; }
public ObservableCollection<Choice> Choices { set; get; }
public QuestionType Qtype { set; get; }
public Answer Ans { set; get; }
public int Marks { set; get; }
}
}
namespace Learn
{
public class Choice
{
public string Name { get; set; }
public bool isChecked { get; set; }
}
}
namespace Learn
{
public class NestedItemsViewModel
{
public NestedItemsViewModel()
{
Questions = new ObservableCollection<Question>();
for (int i = 0; i < 10; i++)
{
Question qn = new Question();
qn.Name = "Qn" + i;
for (int j = 0; j < 4; j++)
{
Choice ch = new Choice();
ch.Name = "Choice" + j;
qn.Choices.Add(ch);
}
Questions.Add(qn);
}
}
public ObservableCollection<Question> Questions { get; set; }
}
public partial class LearnPage : UserControl
{
public LearnPage()
{
InitializeComponent();
this.DataContext = new NestedItemsViewModel();
}
}
}
You initial attempt gets you 80% of the way there. Hopefully, my answer will get you a little closer.
To start with, INotifyPropertyChanged is an interface an object supports to notify the Xaml engine that data has been modified and the user interface needs to be updated to show the change. You only need to do this on standard clr properties.
So if your data traffic is all one way, from the ui to the model, then there is no need for you to implement INotifyPropertyChanged.
I have created an example that uses the code you supplied, I have modified it and created a view to display it. The ViewModel and the data classes are as follows
public enum QuestionType
{
OppositeMeanings,
LinkWords
}
public class Instruction
{
public string Name { get; set; }
public ObservableCollection<Question> Questions { get; set; }
}
public class Question : INotifyPropertyChanged
{
private Choice selectedChoice;
private string instruction;
public Question()
{
Choices = new ObservableCollection<Choice>();
}
public string Name { set; get; }
public bool IsInstruction { get { return !string.IsNullOrEmpty(Instruction); } }
public string Instruction
{
get { return instruction; }
set
{
if (value != instruction)
{
instruction = value;
OnPropertyChanged();
OnPropertyChanged("IsInstruction");
}
}
}
public string Clue { set; get; }
public ObservableCollection<Choice> Choices { set; get; }
public QuestionType Qtype { set; get; }
public Choice SelectedChoice
{
get { return selectedChoice; }
set
{
if (value != selectedChoice)
{
selectedChoice = value;
OnPropertyChanged();
}
}
}
public int Marks { set; get; }
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
{
handler.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class Choice
{
public string Name { get; set; }
public bool IsCorrect { get; set; }
}
public class NestedItemsViewModel
{
public NestedItemsViewModel()
{
Questions = new ObservableCollection<Question>();
for (var h = 0; h <= 1; h++)
{
Questions.Add(new Question() { Instruction = string.Format("Instruction {0}", h) });
for (int i = 1; i < 5; i++)
{
Question qn = new Question() { Name = "Qn" + ((4 * h) + i) };
for (int j = 0; j < 4; j++)
{
qn.Choices.Add(new Choice() { Name = "Choice" + j, IsCorrect = j == i - 1 });
}
Questions.Add(qn);
}
}
}
public ObservableCollection<Question> Questions { get; set; }
internal void SelectChoice(int questionIndex, int choiceIndex)
{
var question = this.Questions[questionIndex];
question.SelectedChoice = question.Choices[choiceIndex];
}
}
Notice that Answer has been changed to a SelectedChoice. This may not be what you require but it made the example a little easier. i have also implemented the INotifyPropertyChanged pattern on the SelectedChoice so I can set the SelectedChoice from code (notably from a call to SelectChoice).
The main windows code behind instantiates the ViewModel and handles a button event to set a choice from code behind (purely to show INotifyPropertyChanged working).
public partial class MainWindow : Window
{
public MainWindow()
{
ViewModel = new NestedItemsViewModel();
InitializeComponent();
}
public NestedItemsViewModel ViewModel { get; set; }
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
ViewModel.SelectChoice(3, 3);
}
}
The Xaml is
<Window x:Class="StackOverflow._20984156.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:learn="clr-namespace:StackOverflow._20984156"
DataContext="{Binding RelativeSource={RelativeSource Self}, Path=ViewModel}"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<learn:SelectedItemIsCorrectToBooleanConverter x:Key="SelectedCheckedToBoolean" />
<Style x:Key="ChoiceRadioButtonStyle" TargetType="{x:Type RadioButton}" BasedOn="{StaticResource {x:Type RadioButton}}">
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource SelectedCheckedToBoolean}">
<Binding Path="IsCorrect" />
<Binding RelativeSource="{RelativeSource Self}" Path="IsChecked" />
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Background" Value="Green"></Setter>
</DataTrigger>
<DataTrigger Value="False">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource SelectedCheckedToBoolean}">
<Binding Path="IsCorrect" />
<Binding RelativeSource="{RelativeSource Self}" Path="IsChecked" />
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Background" Value="Red"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
<DataTemplate x:Key="InstructionTemplate" DataType="{x:Type learn:Question}">
<TextBlock Text="{Binding Path=Instruction}" />
</DataTemplate>
<DataTemplate x:Key="QuestionTemplate" DataType="{x:Type learn:Question}">
<StackPanel Margin="10 0">
<TextBlock Text="{Binding Path=Name}" />
<ListBox ItemsSource="{Binding Path=Choices}" SelectedItem="{Binding Path=SelectedChoice}" HorizontalAlignment="Stretch">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type learn:Choice}">
<RadioButton Content="{Binding Path=Name}" IsChecked="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}, Path=IsSelected}" Margin="10 1"
Style="{StaticResource ChoiceRadioButtonStyle}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</DataTemplate>
</Window.Resources>
<DockPanel>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom">
<Button Content="Select Question 3 choice 3" Click="ButtonBase_OnClick" />
</StackPanel>
<ItemsControl ItemsSource="{Binding Path=Questions}">
<ItemsControl.ItemTemplateSelector>
<learn:QuestionTemplateSelector QuestionTemplate="{StaticResource QuestionTemplate}" InstructionTemplate="{StaticResource InstructionTemplate}" />
</ItemsControl.ItemTemplateSelector>
</ItemsControl>
</DockPanel>
</Window>
Note: My learn namespace is different from yours so if you use this code, you will need to modify it to your namespace.
So, the primary ListBox display a list of Questions. Each item in the ListBox (each Question) is rendered using a DataTemplate. Similarly, in the DataTemplate, a ListBox is used to display the choices and a DataTemplate is used to render each choice as a radio button.
Points of interest.
Each choice is bound to the IsSelected property of the ListBoxItem it belongs to. It may not appear in the xaml but there will be a ListBoxItem for each choice. The IsSelected property is kept in sync with the SelectedItem property of the ListBox (by the ListBox) and that is bound to the SelectedChoice in your question.
The choice ListBox has an ItemsPanel. This allows you to use the layout strategy of a different type of panel to layout the items of the ListBox. In this case, a horizontal StackPanel.
I have added a button to set the choice of question 3 to 3 in the viewmodel. This will show you INotifyPropertyChanged working. If you remove the OnPropertyChanged call from the setter of the SelectedChoice property, the view will not reflect the change.
The example above does not handle the Instruction Type.
To handle instructions, I would either
Insert the instruction as a question and change the question DataTemplate so it does not display the choices for an instruction; or
Create a collection of Instructions in the view model where the Instruction type has a collection of questions (the view model would no longer have a collection of questions).
The Instruction class would be something like
public class Instruction
{
public string Name { get; set; }
public ObservableCollection<Question> Questions { get; set; }
}
Addition based on comment regarding timer expiration and multiple pages.
The comments here are aimed at giving you enough information to know what to search for.
INotifyPropertyChanged
If in doubt, implement INotifyPropertyChanged. My comment above was to let you know why you use it. If you have data already displayed that will be manipulated from code, then you must implement INotifyPropertyChanged.
The ObservableCollection object is awesome for handling the manipulation of lists from code. Not only does it implement INotifyPropertyChanged, but it also implements INotifyCollectionChanged, both of these interfaces ensure that if the collection changes, the xaml engine knows about it and displays the changes. Note that if you modify a property of an object in the collection, it will be up to you to notify the Xaml engine of the change by implementing INotifyPropertyChanged on the object. The ObservableCollection is awesome, not omnipercipient.
Paging
For your scenario, paging is simple. Store the complete list of questions somewhere (memory, database, file). When you go to page 1, query the store for those questions and populate the ObservableCollection with those questions. When you go to page 2, query the store for page 2 questions, CLEAR the ObservableCollection and re populate. If you instantiate the ObservableCollection once and then clear and repopulate it while paging, the ListBox refresh will be handled for you.
Timers
Timers are pretty resource intensive from a windows point of view and as such, should be used sparingly. There are a number of timers in .net you can use. I tend to play with System.Threading.Timer or System.Timers.Timer. Both of these invoke the timer callback on a thread other than the DispatcherThread, which allows you to do work without affecting the UI responsiveness. However, if during the work you need to modify the UI, you will need to Dispatcher.Invoke or Dispatcher.BeginInvoke to get back on the Dispatcher thread. BeginInvoke is asynchronous and therefore, should not hang the thread while it waits for the DispatcherThread to become idle.
Addition based on comment regarding separation of data templates.
I added an IsInstruction to the Question object (I did not implement a Instruction class). This shows an example of raising the PropertyChanged event from property A (Instruction) for Property B (IsInstruction).
I moved the DataTemplate from the list box to the Window.Resources and gave it a key. I also created a second DataTemplate for the instruction items.
I created a DataTemplateSelector to choose which DataTemplate to use. DataTemplateSelectors are good when you need to select a DataTemplate as the Data is being loaded. Consider it a OneTime selector. If you required the DataTemplate to change during the scope of the data it is rendering, then you should use a trigger. The code for the selector is
public class QuestionTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
DataTemplate template = null;
var question = item as Question;
if (question != null)
{
template = question.IsInstruction ? InstructionTemplate : QuestionTemplate;
if (template == null)
{
template = base.SelectTemplate(item, container);
}
}
else
{
template = base.SelectTemplate(item, container);
}
return template;
}
public DataTemplate QuestionTemplate { get; set; }
public DataTemplate InstructionTemplate { get; set; }
}
The selector is bound to the ItemTemplateSelector of the ItemsControl.
Finally, I converted the ListBox into an ItemsControl. The ItemsControl has most of the functionality of a ListBox (the ListBox control is derived from an ItemsControl) but it lacks the Selected functionality. It will make your questions seem more like a page of questions than a list.
NOTE: Although I only added the code of the DataTemplateSelector to the addition, I updated the code snipits throughout the rest of the answer to work with the new DataTemplateSelector.
Addition based on comment regarding setting the background for right and wrong answers
Setting the background dynamically based on values in the model requires a trigger, in this case, multiple triggers.
I have updated the Choice object to include an IsCorrect and during the creation of the questions in the ViewModel, I have assigned IsCorrect on one of the Choices for each answer.
I have also updated the MainWindow to include style triggers on the RadioButton. There are a few points to not about triggers
1. The style or the RadioButton sets the backgound when the mouse is over. A fix would require recreating the style of the RadioButton.
1. Since the trigger is based on 2 values, we can either create another property on the model to combine the 2 properties, or use MultiBinding and a MultValueConverter. I have used the MultiBinding and the MultiValueConverter is as follows.
public class SelectedItemIsCorrectToBooleanConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var boolValues = values.OfType<bool>().ToList();
var isCorrectValue = boolValues[0];
var isSelected = boolValues[1];
if (isSelected)
{
return isCorrectValue;
}
return DependencyProperty.UnsetValue;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
I hope this helps

What do I need to further qualify the DataContext for a binding?

The files I have created and will be referring to in this question are:
TechnicainSelectionView.xaml
TechnicianSelectionView.cs
TechnicianSelectionViewModel.cs
Technician.cs (Code First Entity)
I have the following xaml in my TechnicanSelectionView.xaml
<UserControl xmlns etc... here"
d:DesignHeight="48" d:DesignWidth="300">
<Grid>
<StackPanel>
<Label Content="Select a Technican to run the test" FontWeight="Bold"></Label>
<ComboBox ItemsSource="{Binding Technicians, Mode=TwoWay}"></ComboBox>
</StackPanel>
</Grid>
</UserControl>
The Technicians property to which the ItemSource is set to bind to states that it Cannot resolve Technicians due to an unknown DataContext.
So if we look to my TechnicianSelectionView.cs code-behind...
public partial class TechnicianSelectionView : UserControl
{
public TechnicianSelectionViewModel ViewModel { get; private set; }
public TechnicianSelectionView()
{
InitializeComponent();
Technician.GenerateSeedData();
ViewModel = new TechnicianSelectionViewModel();
DataContext = ViewModel;
}
}
... we see that I am setting the view's DataContext to my TechnicianSelectionViewModel ...
public class TechnicianSelectionViewModel : ViewModelBase
{
public ObservableCollection<Technician> Technicians { get; set; }
public TechnicianSelectionViewModel()
{
Technicians = new ObservableCollection<Technician>();
}
public bool IsLoaded { get; private set; }
public void LoadTechnicians()
{
List<Technician> technicians;
using (var db = new TestContext())
{
var query = from tech in db.Technicians
select tech;
foreach (var technician in query)
{
Technicians.Add(technician);
}
}
IsLoaded = true;
}
}
Techicians is a property on my ViewModel...
So having already set the DataContext for the view, why can't it resolve Technicians on the ViewModel as the DataContext/property it is going to bind to?
EDIT:
As per a concern in a comment below. This is a design time problem and not compile time. I should have indicated this at the start.
You need to specify the type of data context in the xaml to get design-time support. Even though you assigned the data context in code-behind, the designer is not going to recognize that.
Try putting the following in your xaml:
d:DataContext="{d:DesignInstance vm:TechnicianSelectionViewModel}"
See this link for more details.
In my Xamarin Forms Xaml file I used the following lines in the header (ContentPage tag) and it worked perfectly as I wanted.
Basically now
the intellisense shows the fields in the binding
my Resharper is able to rename the binding in the Xaml file if I refactor the name of the property
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vm="clr-namespace:YourApplicationName.ViewModels;assembly=YourApplicationName"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance {x:Type vm:CurrentPageViewModel}}"

A Simple Wpf MVVM Binding Issue

I am trying my hands on WPF MVVM. I have written following code in XAML
<UserControl x:Class="Accounting.Menu"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Accounting"
mc:Ignorable="d"
d:DesignHeight="105" d:DesignWidth="300">
<UserControl.DataContext>
<local:MenuViewModel/>
</UserControl.DataContext>
<StackPanel>
<StackPanel>
<TextBlock Text="{Binding Path=MenuHeader}"/>
</StackPanel>
<ListBox ItemsSource="{Binding Path=MenuItems}" Height="70"/>
</StackPanel>
</UserControl>
I have got a MenuViewModel with properties MenuHeader and MenuItems. I get values in both the properties during runtime. Former is bound to text of TextBlock and latter to ItemSource of ListBox. But when I run the solution, TextBlock and ListBox are empty.
Edit: Code of ViewModel
public class MenuViewModel: ViewModelBase
{
AccountingDataClassesDataContext db;
private string _menuType;
public string MenuHeader { get; set; }
public ObservableCollection<string> MenuItems { get; set; }
public MenuViewModel()
{
}
public MenuViewModel(string menuType)
{
this._menuType = menuType;
db = new AccountingDataClassesDataContext();
if (menuType == "Vouchers")
{
var items = db.Vouchers.OrderBy(t => t.VoucherName).Select(v => v.VoucherName).ToList<string>();
if (items.Any())
{
MenuItems = new ObservableCollection<string>(items);
MenuHeader = "Vouchers";
}
}
else
{
System.Windows.MessageBox.Show("Menu not found");
}
}
}
Thanks in advance.
You are creating your ViewModel in the XAML using your ViewModel's default contructor which does nothing. All your population code is in the non-default contructor which is never called.
The more usual way is to create the ViewModel in code, and inject it into the view either explicitly using View.DataContext = ViewModel, or impllcitly using a DataTemplate.
I think you have to trigger the OnPropertyChanged event. I am not sure if you are using a MVVM library (since you inherit from ViewModelBase you might be using MVVM Light for example), there they wrap the OnPropertyChanged in the RaisePropertyChanged event handler.
Triggering the event will inform WPF to update the UI.
string m_MenuHeader;
public string MenuHeader
{
get
{
return m_MenuHeader;
}
set
{
m_MenuHeader=value; OnPropertyChanged("MenuHeader");
}
}

Categories