MVVM Navigation Parent and Child Views - c#

I have a GeneralView that is a Parent View and when opens it opens the Parent followed by a Child. I want to implement navigation and keep the buttons in the side (like a UserPage). Lets go to the desired behavior and followed by the code I have now.
How I have implemented the ChildView don't change, stays in the HomeView aka FriendsView.
So description Login > GeneralView (that Opens Immediately in the Home) > Click in About and the childView changes to the AboutView, click in home the HomeView is showed again.
What I have:
GeneralView
<UserControl x:Class="WpfWHERE.View.GeneralView"
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:WpfWHERE.View"
xmlns:ViewModel="clr-namespace:WpfWHERE.ViewModel"
mc:Ignorable="d"
d:DesignHeight="600" d:DesignWidth="800">
<UserControl.DataContext>
<ViewModel:GeneralViewModel/>
</UserControl.DataContext>
<UserControl.Resources>
<DataTemplate DataType="{x:Type ViewModel:FriendsViewModel}">
<local:FriendsView />
</DataTemplate>
<DataTemplate DataType="{x:Type ViewModel:AboutViewModel}">
<local:AboutView />
</DataTemplate>
</UserControl.Resources>
<DockPanel Margin="0,0,0,0">
<StackPanel Orientation="Horizontal">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MaxWidth="200"/>
</Grid.ColumnDefinitions>
<Image Grid.Column="1" x:Name="userImage" Source="/Resources/Images/profileImage.png" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Height="161" Width="180" />
<Label Grid.Column="1" x:Name="labelName" Content="NameHere" HorizontalAlignment="Left" Margin="10.4,171,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.536,1.344" Height="26" Width="67"/>
<StackPanel Grid.Column="1" Margin="0.4,202,-1.2,0" VerticalAlignment="Top" Height="200">
<MenuItem Style="{DynamicResource MyMenuItem}" Command="{Binding DataContext.SelectViewCommand, ElementName=GeneralView}" CommandParameter="FriendsViewModel" Header="Home" x:Name="Home"/>
<MenuItem Style="{DynamicResource MyMenuItem}" Header="Overview" x:Name="Overview"/>
<MenuItem Style="{DynamicResource MyMenuItem}" Header="Settings" x:Name="Settings"/>
<MenuItem Style="{DynamicResource MyMenuItem}" Header="About" Command="{Binding DataContext.SelectViewCommand, ElementName=GeneralView}" CommandParameter="AboutViewModel" x:Name="About"/>
</StackPanel>
</Grid>
<ContentControl Content="{Binding Current_ViewModel}" Height="600" Width="600"/>
</StackPanel>
</DockPanel>
GeneralViewModel
class GeneralViewModel:AViewModel
{
public GeneralViewModel()
{
this.AddViewModel(new FriendsViewModel() { DisplayName = "Friends", InternalName = "FriendsViewModel" });
this.AddViewModel(new AboutViewModel() { DisplayName = "About", InternalName = "AboutViewModel" });
this.Current_ViewModel = this.GetViewModel("FriendsViewModel");
}
}
AViewModel Interface
public abstract class AViewModel : ViewModelBase
{
public string Name { get; set; }
public RelayCommand<string> SelectViewCommand { get; set; }
public AViewModel()
{
SelectViewCommand = new RelayCommand<string>(OnSelectViewCommand);
}
private static ObservableCollection<ViewModelBase> _ViewModels;
public static ObservableCollection<ViewModelBase> ViewModels
{
get { return _ViewModels; }
set { _ViewModels = value; }
}
public void AddViewModel(ViewModelBase viewmodel)
{
if (ViewModels == null)
ViewModels = new ObservableCollection<ViewModelBase>();
if (!ViewModels.Contains(viewmodel))
ViewModels.Add(viewmodel);
}
public ViewModelBase GetViewModel(string viewmodel)
{
return ViewModels.FirstOrDefault(item => item.InternalName == viewmodel);
}
private void OnSelectViewCommand(string obj)
{
switch (obj)
{
case "ExitCommand":
Application.Current.Shutdown();
break;
default:
this.Current_ViewModel = this.GetViewModel(obj);
break;
}
}
private ViewModelBase _Current_ViewModel;
public ViewModelBase Current_ViewModel
{
get { return _Current_ViewModel; }
set { _Current_ViewModel = value; OnPropertyChanged("Current_ViewModel"); }
}
}

Try this....
Change this...
<StackPanel Grid.Column="1" Margin="0.4,202,-1.2,0" VerticalAlignment="Top" Height="200">
<MenuItem Style="{DynamicResource MyMenuItem}" Command="{Binding DataContext.SelectViewCommand, ElementName=GeneralView}" CommandParameter="FriendsViewModel" Header="Home" x:Name="Home"/>
<MenuItem Style="{DynamicResource MyMenuItem}" Header="Overview" x:Name="Overview"/>
<MenuItem Style="{DynamicResource MyMenuItem}" Header="Settings" x:Name="Settings"/>
<MenuItem Style="{DynamicResource MyMenuItem}" Header="About" Command="{Binding DataContext.SelectViewCommand, ElementName=GeneralView}" CommandParameter="AboutViewModel" x:Name="About"/>
</StackPanel>
To this...
<StackPanel Grid.Column="1" Margin="0.4,202,-1.2,0" VerticalAlignment="Top" Height="200">
<MenuItem Style="{DynamicResource MyMenuItem}" Command="{Binding SelectViewCommand}" CommandParameter="FriendsViewModel" Header="Home" x:Name="Home"/>
<MenuItem Style="{DynamicResource MyMenuItem}" Header="Overview" x:Name="Overview"/>
<MenuItem Style="{DynamicResource MyMenuItem}" Header="Settings" x:Name="Settings"/>
<MenuItem Style="{DynamicResource MyMenuItem}" Header="About" Command="{Binding SelectViewCommand}" CommandParameter="AboutViewModel" x:Name="About"/>
</StackPanel>
Note that I have removed 'DataContext' and 'ElementName' from your MenuItems
INotifyProperty is already implemented in ViewModelBase
UPDATE 1
The problem was with ElementName=GeneralView... an Element with that Name does not exist. you could have added x:Name=”GeneralView” to the top of your Base_View XAML BUT there is no need as your ContentControl was bound to Current_ViewModel in the Base_ViewModel anyway....
When you press a button to 'change Views' you are actually changing the value of the property that your ContentControl is bound to, so you have to call the correct SelectViewCommand function in the SAME instance of the class that your ContentControl is bound too....
In the demo you'll see that in the 'LogOn_View' I call
Command="{Binding DataContext.SelectViewCommand, ElementName=Base_V}", CommandParameter="Main_ViewModel"
Here I am calling the SelectViewCommand in the Base_V, That's because I want to change the view that is displayed in the Base_V's ContentControl
In Main_View I call
Command="{Binding SelectViewCommand}", CommandParameter="MainV1_ViewModel"
Here I am calling the SelectViewCommand in the Main_ViewModel, That's because I want to change the View displayed in the ManiView's ContentControl
For anyone that wants the demo code that I am talking about above, you can find it here...
http://www.mediafire.com/download/3bubiq7s6xw7i73/Navigation1.rar
Also, a little update to the code... replace the AddViewModel function in AviewModel with this.....
public void AddViewModel(ViewModelBase viewmodel)
{
if (ViewModels == null)
ViewModels = new ObservableCollection<ViewModelBase>();
var currentVNs = (from vms in ViewModels where vms.InternalName == viewmodel.InternalName select vms).FirstOrDefault();
if (currentVNs == null)
ViewModels.Add(viewmodel);
}

OK so... After some more thought.... The problem with a lot of tutorial code on the Internet is that it is quite basic. The implementation of the relationship between the Data (Model) <=> View, seems to be a matter of taste. HOWEVER I have only EVER seen it done badly. Personally speaking, most of the WPF applications I have worked on have been absolute rubbish, unmaintainable and unmanageable spaghetti code....You can clearly see that the original developer has 'Learnt' as they went along, everything is everywhere and there is no consistency.... As I normally come on to a project half way though I have no choice other than to continue with the way its been started, so I never gave it much thought, until now!!.
My own understanding is that the 'Model' defines the Data Structure, the 'View' displays that Data to the user and the ViewModel is the bit in-between that translates the Data (Model) into something that can be displayed to the user.
In the ViewModel you would simply have ObservableCollections (Lists of Data (Models)) and single instances of data (a single instance of a Model). The 'View' then Binds to the ObservableCollections (and single Model instances) to display that data (using the magic of XAML and Templating etc.)....
As for passing an object (Class) to the ViewModel, I don't think you would actually need to do that. You would simply create a property in your ViewModel that represents the Data you want to display, then retrieve the Data from a 'Source' as and when needed (typically when the View is displayed but could also be periodicity on a timer\thread or something)....
The main problem with my Demo code (download) was that the constructor of the ViewModels was NEVER being called after the application had started so there was no way of refreshing the Properties, in the ViewModel, from a Data source....
There may very well be a better way to do this BUT I have fixed that problem by introducing an Event called Initialize in the 'ViewModelBase'
public delegate void MyEventHadler();
public event MyEventHadler Initialize;
public void InitializeFunction()
{
if (Initialize != null)
Initialize.Invoke();
}
that Event can then be subscribed to in the constructor of each ViewModel
public MainV2_ViewModel()
{
this.Initialize += MainV2_ViewModel_Initialize; // our new Event
}
And the Event Stub that is called when we navigate to this ViewModel\View from somewhere else....
private void MainV2_ViewModel_Initialize()
{
// So here we are retrieving a List of All users from the WCF Service
this.AllUsers = new ObservableCollection<ServiceReference1.User>( DataAccessLevel.sr1.GetAllUsers());
// Now our AllUsers property has been updated and the View will display the new data
}
Now when you switch from one ViewModel\View to another, the Initialize Event is called\raised in AviewModel\Current_ViewModel Property Setter
private ViewModelBase _Current_ViewModel;
public ViewModelBase Current_ViewModel
{
get { return _Current_ViewModel; }
set {
_Current_ViewModel = value;
// the Constructor of the ViewModel never gets called more that once on App Start
// so we have to implement/raise our own event when changing from one View to another.
if (Current_ViewModel != null)
Current_ViewModel.InitializeFunction(); // InitializeFunction will fire the event in this ViewModel, we can now initialise the properties.
OnPropertyChanged("Current_ViewModel"); }
}
this will then fire the 'Initialize' Event in the ViewModel that is being Switched too, giving us the opportunity to refresh the Data.....
The Demo Code is now a fully functioning application (still needs work of course). It provides the following functionality...
Register a New User :: Create a new user with UserName and Password
Log a User On :: Log on existing User with UserName and Password
Recover a User (forgotten password) :: Reset the Password of a registered User (using existing UserName and new Password)
In addition, error messages are returned from the WCF Service that are then displayed in an Error View (See LogOnError_ViewModel and LogOnError_View) when something goes wrong (incorrect Password etc.)
find the Demo Code here....
http://www.mediafire.com/download/881yo6reo55tm8l/Navigation1_WCF_EF_05052016.rar
The Demo Code comes with a WCF Service and a WPF application, because it uses a WCF Service it can only be run from the Visual Studio IDE (Unless you deploy the WCF service to IIS). The WCF service uses Entity Framework and (should) create\attach a database to store User Data when you first register a new user...
Maybe you can improve on the code or get some ideas.....

Related

Binding Button in DataGrid header to ViewModel

I have a DataGrid that is bound to an ICollectionView "Employees" in my ViewModel, which I want to filter for each column.
Here's what the XAML looks like:
<DataGrid ItemsSource="{Binding Employees}"
attachedBehaviors:DataGridColumnsBehavior.BindableColumns="{Binding EmployeeColumns}">
<DataGrid.ColumnHeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding}" Grid.Column="0"/>
<Button Content="v" Grid.Column="1" Click="ButtonClick"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.ColumnHeaderStyle>
</DataGrid>
This works fine and when calling ButtonClick I could pass the data context along to my ViewModel and search for this string. However, what I would prefer is to bind the button to an event in my ViewModel, so I can get a reference to the column from which this event originated and appropriately handle my data there.
My ViewModel looks like this:
class ViewModel : ChangeNotifier
{
public ICollectionView Employees { get; private set; }
public ViewModel()
{
var _employees = new List<Employee>{...
Employees = CollectionViewSource.GetDefaultView(_employees);
EmployeeColumns = new ObservableCollection<DataGridColumn>();
EmployeeColumns.Add(new DataGridTextColumn { Header = "First Name", Binding = new Binding("FirstName") });
EmployeeColumns.Add(new DataGridTextColumn { Header = "Last Name", Binding = new Binding("LastName") });
FilterMenu = new RelayCommand(new Action<object>(FilterContextMenu));
}
private ICommand filtermenu;
public ICommand FilterMenu
{
get
{
return filtermenu;
}
set
{
filtermenu = value;
}
}
public static void FilterContextMenu(object obj)
{
MessageBox.Show("Event Fired!");
}
public ObservableCollection<DataGridColumn> EmployeeColumns { get; private set; }
}
So my question is: How do I bind to the FilterContextMenu event?
I've tried:
<Button Content="v" Grid.Column="1" Command="{Binding FilterMenu}"/>
And also:
<Button Content="v" Grid.Column="1" Command="{Binding FilterMenu, RelativeSource= {RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}}"/>
None of which triggered the event.
Edit: I probably should add that the greater goal is to create a button that, when clicked, creates a dynamically populated context menu. I may be on the completely wrong track here.
I don't follow your description of the greater goal and dynamically populated context menu.
That sounds like it will have it's own set of somewhat more challenging problems getting a reference to any datacontext in the window. A contextmenu usually relies on placement target to get anything contextual because it isn't part of the window.
If you intend passing commands through from that then this could well get quite complicated.
But... that's a different question entirely.
Binding a button to the command which is in the datacontext of the datagrid.
My datagrid is called dg ( I'm a minimalist at heart ).
I got a bit wordy with my command and it's called ColHeaderCommand.
This works for me:
<DataGridTextColumn.Header>
<Button Content="Title" Command="{Binding DataContext.ColHeaderCommand, ElementName=dg}"/>
</DataGridTextColumn.Header>
My viewmodel is the datacontext of the window, which is inherited down to the datagrid.
You will want some sort of a parameter for that command to pass in what column or whatever you're doing whatever with.

XAML inconsistent binding

I have a custom UserControl called ReferencedItem. It should take a Guid called ItemId. It is implemented as such:
private static void OnItemIdChanged(DependencyObject dobj, DependencyPropertyChangedEventArgs dpArgs)
{
//Do something
}
public static readonly DependencyProperty ItemIdProperty = DependencyProperty.Register("ItemId", typeof(Guid?), typeof(ReferencedItem), new FrameworkPropertyMetadata(
// use an empty Guid as default value
Guid.Empty,
// tell the binding system that this property affects how the control gets rendered
FrameworkPropertyMetadataOptions.AffectsRender,
// run this callback when the property changes
OnItemIdChanged
));
public Guid? ItemId
{
get { return (Guid?)GetValue(ItemIdProperty); }
set { SetValue(ItemIdProperty, value); }
}
public ReferencedItem()
{
InitializeComponent();
ViewModel = new ReferencedItemCtrlViewModel();
DataContext = ViewModel;
}
The ItemsSource will be made up of Reference objects defined as:
public class Reference
{
public Guid Id { get; set; }
}
Now when binding this ReferencedItem the value is not set as intended. Here is the code I want to work, but does not bind as intended:
<ItemsControl x:Name="ReferenceStack" ItemsSource="{Binding References}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:ReferencedItem ItemId="{Binding Id}" Height="30" HorizontalAlignment="Stretch" VerticalAlignment="Top"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I have tried:
<local:ReferencedItem ItemId="128d48f0-f061-49fb-af49-b8e4ef891d03" Height="30" HorizontalAlignment="Stretch" VerticalAlignment="Top"/>
This works as expected, the OnItemIdChanged method is triggered.
<Label Content="{Binding Id}" HorizontalAlignment="Left" VerticalAlignment="Top" Height="30" Width="90"/>
This works as expected, a label is rendered with the Id.
Is there something I'm missing here? From what I can tell the data is available at bind time -- it just doesn't bind under the exact conditions I need it to :)
Thanks for any input!
EDIT:
Here is the code-behind for ReferencedItemList, the first block of XAML posted above:
public partial class ReferencedItemList : UserControl
{
protected ReferencedItemListCtrlViewModel ViewModel;
public ReferencedItemList()
{
InitializeComponent();
ViewModel = new ReferencedItemListCtrlViewModel();
DataContext = ViewModel;
}
public void Load(Guid id, string name)
{
ViewModel.Load(id, name);
//ReferenceStack.ItemsSource = ViewModel.References;
}
}
The commented line has been experimented with in place of the ItemsSource="{Binding References}" that was defined in the XAML.
I don't think I can successfully post the code for ReferencedItemListCtrlViewModel without going down a rabbit hole -- needless to say it has a property References of type ObservableCollection<Reference> where Reference is defined earlier in this post.
ReferencedItem.xaml:
<v:BaseUserControl.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
</v:BaseUserControl.Resources>
<StackPanel Orientation="Horizontal">
<Image x:Name="LinkIcon" Visibility="{Binding HasReference, Converter={StaticResource BooleanToVisibilityConverter}}" ToolTip="View Referenced Item" Source="/Images/link.png" Height="18" MouseUp="LinkIcon_MouseUp"/>
<TextBlock x:Name="ReferencedObjectDesc" Text="{Binding ReferenceHierarchy}" FontStyle="Italic" VerticalAlignment="Center" />
</StackPanel>
I just wanted to post the answer (explanation) I came across.
The problem was changing the DataContext of my ReferencedItem user control in the constructor. The view would instantiate a ReferencedItem and alter the DataContext - so once it was time to bind I had already flipped the context from the intended Reference.
There are multiple ways to resolve the timing - all project dependent. Either avoid setting the DataContext all together, set it post binding, or change the context on other items as appropriate.
Much thanks to Sinatr, Andrew Stephens, and Mike Strobel for all mentioning this at one point or another -- just took me some time to actually reach it. I don't think there's a way to assign credit to a comment, but let me know if there is.

x:Bind ViewModel method to an Event inside DataTemplate

I'm basically asking the same question as this person, but in the context of the newer x:Bind.
ViewModels' DataContext is defined like so
<Page.DataContext>
<vm:ChapterPageViewModel x:Name="ViewModel" />
</Page.DataContext>
So whenever I need to bind something I do it explicitely to the ViewModel like so
ItemsSource="{x:Bind ViewModel.pageList, Mode=OneWay}"
However that doesn't work within templates
<FlipView ItemsSource="{x:Bind ViewModel.pageList, Mode=OneWay}">
<FlipView.ItemTemplate>
<DataTemplate x:DataType="models:Image">
<ScrollViewer SizeChanged="{x:Bind ViewModel.PageResized}"> <-- this here is the culprit
<Image Source="{x:Bind url}"/>
</ScrollViewer>
</DataTemplate>
</FlipView.ItemTemplate>
</FlipView>
Reading the documentation, I found that using Path should basically reset the context to the page, but this (x:Bind Path=ViewModel.PageResizeEvent didn't work either. I'm still getting Object reference not set to an instance of an object, which should mean that it doesn't see the method (but a null).
Image class:
public class Image {
public int page { get; set; }
public string url { get; set; }
public int width { get; set; }
public int heigth { get; set; }
}
And in the ChapterPageViewModel
private List<Image> _pageList;
public List<Image> pageList {
get { return _pageList; }
set { Set(ref _pageList, value); }
}
public override async Task OnNavigatedToAsync(object parameter, NavigationMode mode,
IDictionary<string, object> suspensionState)
{
Initialize();
await Task.CompletedTask;
}
private async void Initialize()
{
pageList = await ComicChapterGet.GetAsync(_chapterId);
}
public void PageResized(object sender, SizeChangedEventArgs e)
{
//resizing logic happens here
}
We have two problems here:
First, trying to directly bind an event to a event handler delegate
That will never work, simply put.
One way to handle an event on MVVM pattern is by using EventTrigger and ICommand.
It requires a class that implements ICommand. This post will help you if don't know how to do it. I'll call mine DelegateCommand.
Here's how I would refactor it in two steps:
1) Add a Command to the VM:
public class ChapterPageViewModel
{
public ChapterPageViewModel()
{
this.PageResizedCommand = new DelegateCommand(OnPageResized);
}
public DelegateCommand PageResizedCommand { get; }
private void OnPageResized()
{ }
}
2) Bind that Command to the SizeChanged event with EventTrigger and InvokeCommandAction.
<Page (...)
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:core="using:Microsoft.Xaml.Interactions.Core">
(...)
<FlipView ItemsSource="{x:Bind ViewModel.pageList, Mode=OneWay}" >
<FlipView.ItemTemplate>
<DataTemplate x:DataType="models:Image">
<ScrollViewer>
<i:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="SizeChanged">
<core:InvokeCommandAction
Command="{x:Bind ViewModel.PageResizedCommand }" />
</core:EventTriggerBehavior>
</i:Interaction.Behaviors>
<Image Source="{x:Bind url}"/>
</ScrollViewer>
</DataTemplate>
</FlipView.ItemTemplate>
</FlipView>
</Page>
"But Gabriel", you say, "that didn't work!"
I know! And that's because of the second problem, which is trying to x:Bind a property that does not belong to the DataTemplate class
This one is closely related to this question, so I´ll borrow some info from there.
From MSDN, regarding DataTemplate and x:Bind
Inside a DataTemplate (whether used as an item template, a content
template, or a header template), the value of Path is not interpreted
in the context of the page, but in the context of the data object
being templated. So that its bindings can be validated (and efficient
code generated for them) at compile-time, a DataTemplate needs to
declare the type of its data object using x:DataType.
So, when you do <ScrollViewer SizeChanged="{x:Bind ViewModel.PageResized}">, you're actually searching for a property named ViewModel on the that models:Image class, which is the DataTemplate's x:DataType. And such a property does not exist on that class.
Here, I can see two options. Choose one of them:
Add that ViewModel as a property on the Image class, and fill it up on the VM.
public class Image {
(...)
public ChapterPageViewModel ViewModel { get; set; }
}
public class ChapterPageViewModel
{
(...)
private async void Initialize() {
pageList = await ComicChapterGet.GetAsync(_chapterId);
foreach(Image img in pageList)
img.ViewModel = this;
}
}
With only this, that previous code should work with no need to change anything else.
Drop that x:Bind and go back to good ol'Binding with ElementName.
<FlipView ItemsSource="{x:Bind ViewModel.pageList, Mode=OneWay}" x:Name="flipView">
<FlipView.ItemTemplate>
<DataTemplate x:DataType="models:Image">
<ScrollViewer>
<i:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="SizeChanged">
<core:InvokeCommandAction
Command="{Binding DataContext.PageResizedCommand
, ElementName=flipView}" />
</core:EventTriggerBehavior>
</i:Interaction.Behaviors>
<Image Source="{x:Bind url}"/>
</ScrollViewer>
</DataTemplate>
</FlipView.ItemTemplate>
</FlipView>
This one kind of defeat the purpose of your question, but it does work and it's easier to pull off then the previous one.

Databinding a class with boolean and text to XMAL

I submitted this accidently, I fixed it myself and wasn't going to submit after writing out the question. But have learnt from the comments, thanks!
I am trying to create a simple todo app in win8 and eventually want to hock it into ToDoIst API.
I have created a simple task class to try and get my head around the databinding however I just can not get it to do what I want to do. I have used listboxes and other basic form elements.
task.cs
class task
{
private string content;
private bool complete;
public string Content
{
get {return content;}
set { content = value; }
}
public bool Complete
{
get { return complete; }
set { complete = value; }
}
public task(string content)
{
Content = content;
Complete = false;
}
}
MainPage.xaml
And at the moment my XAML looks like this.
<GridView HorizontalAlignment="Left" Margin="482,190,0,0" VerticalAlignment="Top" Width="400" Height="500">
<ListView x:Name="LVtasks" HorizontalAlignment="Left" Height="500" VerticalAlignment="Top" Width="400" ItemsSource="{Binding}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Content}"/>
<RadioButton/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</GridView>
I have put in some dummy data, 4 elements and when I run it, it comes up with 4 boxes with radio buttons however no text (there is space for the text) I am not sure how I would bind the bool?
I can not see what I am doing wrong. If anyone could help and point me in the right direction, I have searched a fair amount of tutorials and just can not figure it out.
Your code looks a little strange, maybe this is what you want:
<ListView x:Name="LVtasks" HorizontalAlignment="Left" Height="500" VerticalAlignment="Top" Width="400" ItemsSource="{Binding ToDoItems}">
<ListView.ItemTemplate>
<DataTemplate>
<RadioButton GroupName="ToDos" Content="{Binding Content}" IsChecked="{Binding IsComplete}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Do you really want radiobuttons? I think you want Checkboxes, the difference is that when you use radiobuttons only one in a group can be 'checked'
I used this code behind to have a datacontext:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
ToDoItems = new ObservableCollection<TodoItem>(new List<TodoItem>
{
new TodoItem("Content1"),
new TodoItem("Content2")
});
this.DataContext = this;
}
public ObservableCollection<TodoItem> ToDoItems { get; set; }
}
I changed the name of task to ToDoItem Task is already a class in the framework and might cause confusion.
For the RadioButton IsChecked is the property to bind to a bool property:
<RadioButton IsChecked="{Binding Path=Complete}"/>
Your text is most likely not showing up because you haven't set up any change notifications and the binding is happening before you set the Content values. Using the INotifyPropertyChanged interface is the most common and usually the easiest way to do this.
Like John already mentioned you should really let the window containing your listview implement the INotifyPropertyChanged interface. And to set the data context of your window like Johan said. It is important that you call the propertychanged method in each setter of a property. It is also useful to use an ObservableCollection as ItemSource of your listview. Try to create an instance of ObservableCollection, create a property for it calling propertychanged method in its setter an set rhe ItemSource of your listview to the property. Do not forget to also call propertychanged whenever you add or remove items from the collection

WPF: How to bind a command to the ListBoxItem using MVVM?

I have just started learning MVVM. I've made the application from scratch by following this MVVM tutorial (I highly recommend it to all MVVM beginners out there). Basically, what I have created so far is a couple of text boxes where user adds his or her data, a button to save that data which subsequently populates the ListBox with all entries made.
Here's where I got stuck: I want to be able to double-click on a ListBoxItem and to trigger a command that I have created and added to my ViewModel. I don't know how to finish the XAML side, i.e. I don't know how to bind that command to the ListBox(Item).
Here's XAML:
...
<ListBox
Name="EntriesListBox"
Width="228"
Height="208"
Margin="138,12,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
ItemsSource="{Binding Entries}" />
...
Here's ViewModel:
public class MainWindowViewModel : DependencyObject
{
...
public IEntriesProvider Entries
{
get { return entries; }
}
private IEntriesProvider entries;
public OpenEntryCommand OpenEntryCmd { get; set; }
public MainWindowViewModel(IEntriesProvider source)
{
this.entries = source;
...
this.OpenEntryCmd = new OpenEntryCommand(this);
}
...
}
And finally, here's the OpenEntryCommand that I want to be executed once the user double-clicks the item in the EntriesListBox:
public class OpenEntryCommand : ICommand
{
private MainWindowViewModel viewModel;
public OpenEntryCommand(MainWindowViewModel viewModel)
{
this.viewModel = viewModel;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter)
{
return parameter is Entry;
}
public void Execute(object parameter)
{
string messageFormat = "Subject: {0}\nStart: {1}\nEnd: {2}";
Entry entry = parameter as Entry;
string message = string.Format(messageFormat,
entry.Subject,
entry.StartDate.ToShortDateString(),
entry.EndDate.ToShortDateString());
MessageBox.Show(message, "Appointment");
}
}
Please help, I'd appreciate it.
Unfortunately, only ButtonBase derived controls have the possibility for binding ICommand objects to their Command properties (for the Click event).
However, you can use an API provided by Blend to map an event (like in your case MouseDoubleClick on the ListBox) to an ICommand object.
<ListBox>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<i:InvokeCommandAction Command="{Binding YourCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBox>
You'll have to define: xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" and have a reference to System.Windows.Interactivity.dll.
-- EDIT --
This is part of WPF4, but u can use Microsoft.Windows.Interactivity if you're not using WPF4. This dll is from Blend SDK, which doesn't require Blend, from here:
http://www.microsoft.com/downloads/en/details.aspx?FamilyID=f1ae9a30-4928-411d-970b-e682ab179e17&displaylang=en
Update: I found something that should help you. check this link on MVVM Light Toolkit which contains a walkthrough on how to do this, along with a link to the needed libraries. MVVM Light Toolkit is a very interesting framework for applying MVVM with Silverlight, WPF, and WP7.
Hope this helps :)
This is made tricky because of the DoubleClick event. There are a few ways to do this:
Handle the double-click event in code behind, and then manually invoke a command/method on your ViewModel
Use an attached behavior to route the DoubleClick event to your Command
Use a Blend Behavior to map the DoubleClick event to your command
2 and 3 might be more pure, but frankly, 1 is easier, less complex, and not the worst thing in the world. For a one-off case, I'd probably use approach #1.
Now, if you changed your requirements to use, say, a hyperlink on each item, it would be easier. Start out by naming the root element in your XAML - e.g., for a Window:
<Window .... Name="This">
Now, in the DataTemplate for your ListBox items, use something like this:
<ListBox ...>
<ListBox.ItemTemplate>
<DataTemplate>
<Hyperlink
Command="{Binding ElementName=This, Path=DataContext.OpenEntryCmd}"
Text="{Binding Path=Name}"
/>
The ElementName binding lets you resolve the OpenEntryCmd from the context of your ViewModel, rather than the specific data item.
EDIT: I wrote this post as an inexperienced WPF developer, nowadays I'd either use a framework that provides event to command binding, or simply use a button and restyle it. Of course for maximum flexibility this is maybe better.
I find the best way to do this is to create a simple user control wrapper for my content, with dependency properties for the command and parameter.
The reason I did this was due to the Button not bubbling the click event to my ListBox which prevented it from selecting the ListBoxItem.
CommandControl.xaml.cs:
public partial class CommandControl : UserControl
{
public CommandControl()
{
MouseLeftButtonDown += OnMouseLeftButtonDown;
InitializeComponent();
}
private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs mouseButtonEventArgs)
{
if (Command != null)
{
if (Command.CanExecute(CommandParameter))
{
Command.Execute(CommandParameter);
}
}
}
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register("Command", typeof(ICommand),
typeof(CommandControl),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.None));
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register("CommandParameter", typeof(object),
typeof(CommandControl),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.None));
public object CommandParameter
{
get { return (object)GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
}
CommandControl.xaml:
<UserControl x:Class="WpfApp.UserControls.CommandControl"
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="300" d:DesignWidth="300"
Background="Transparent">
</UserControl>
Usage:
<ListBoxItem>
<uc:CommandControl Command="{Binding LoadPageCommand}"
CommandParameter="{Binding HomePageViewModel}">
<TextBlock Text="Home" Margin="0,0,0,5" VerticalAlignment="Center"
Foreground="White" FontSize="24" />
</uc:CommandControl>
</ListBoxItem>
The Content can be whatever, and when the control is clicked, it will execute the command.
EDIT: Added Background="Transparent" to UserControl to enable click events on the entire area of the control.
This is a bit of a hack, but it works well and allows you to use commands and avoid code behind. This also has the added benefit of not triggering the command when you double-click (or whatever your trigger is) in the empty ScrollView area assuming your ListBoxItems don't fill the entire container.
Basically, just create a DataTemplate for your ListBox that is composed of a TextBlock and bind the width of the TextBlock to the width of the ListBox, set the margins and padding to 0, and disable horizontal scrolling (because the TextBlock will bleed beyond the visible bounds of the ScrollView triggering the horizontal scroll bar otherwise). The only bug I've found is that the command won't fire if the user clicks precisely on the border of the ListBoxItem, which I can live with.
Here is an example:
<ListBox
x:Name="listBox"
Width="400"
Height="150"
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
ItemsSource="{Binding ItemsSourceProperty}"
SelectedItem="{Binding SelectedItemProperty}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Padding="0"
Margin="0"
Text="{Binding DisplayTextProperty}"
Width="{Binding ElementName=listBox, Path=Width}">
<TextBlock.InputBindings>
<MouseBinding
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBox}}, Path=DataContext.SelectProjectCommand}"
Gesture="LeftDoubleClick" />
</TextBlock.InputBindings>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I recently needed to trigger an ICommand upon double clicking a ListBoxItem as well.
Personally, I don't like the DataTemplate method as it is binding to the content inside the ListBoxItem container, and not the container itself. I've opted to use an Attached Property to assign an InputBinding on the container. It takes a little more elbow grease, but it works well.
First, we need to create an attached property class. I've created mine a little more generically towards any class that derives from FrameworkElement, just in case I run into this again with a different visual.
public class FrameworkElementAttachedProperties : DependencyObject
{
public static readonly DependencyProperty DoubleClickProperty = DependencyProperty.RegisterAttached("DoubleClick", typeof(InputBinding),
typeof(FrameworkElementAttachedProperties), new PropertyMetadata(null, OnDoubleClickChanged));
public static void SetDoubleClick(FrameworkElement element, InputBinding value)
{
element.SetValue(DoubleClickProperty, value);
}
public static InputBinding GetDoubleClick(FrameworkElement element)
{
return (InputBinding)element.GetValue(DoubleClickProperty);
}
private static void OnDoubleClickChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
FrameworkElement element = obj as FrameworkElement;
/// Potentially throw an exception if an object is not a FrameworkElement (is null).
if(e.NewValue != null)
{
element.InputBindings.Add(e.NewValue as InputBinding);
}
if(e.OldValue != null)
{
element.InputBindings.Remove(e.OldValue as InputBinding);
}
}
}
Then the final step is to override the base container style for the ListBoxItem.
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}"
BasedOn="{StaticResource ListBoxItem}">
<Setter Property="local:FrameworkElementAttachedProperties.DoubleClick">
<Setter.Value>
<MouseBinding Command="{Binding OnListBoxItemDoubleClickCommand}"
MouseAction="LeftDoubleClick"/>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
Now, anytime a ListBoxItem is double clicked, it will fire our OnListBoxItemDoubleClickCommand.
If you're looking for a nice simple solution that uses interactions instead of mucking about with user controls, code behind, input bindings, custom attached properties, etc.
And you want something that works at the ListBoxItem level, i.e. not ListBox level as per the (incorrectly) accepted solution.
Then here's a snippet for a simple 'button like' click action..
<ListBox>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Background="Transparent">
<!-- insert your visuals here -->
<b:Interaction.Triggers>
<b:EventTrigger EventName="MouseUp">
<b:InvokeCommandAction Command="{Binding YourCommand}" />
</b:EventTrigger>
</b:Interaction.Triggers>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Note, background="Transparent" is required to ensure the entire Grid is clickable and not just the contents inside.

Categories

Resources