Include a close button in a tab item dynamically using mvvm - c#

I have the a tabcontrol in my MainWindow. The default/first tab of the tabcontrol is the home user control. In the home page I have a button that can add further tabs.
MainWindow.xaml:
<Window.Resources>
<DataTemplate x:Key="ClosableTabItemTemplate">
<Button Content="X" Cursor="Hand" DockPanel.Dock="Right" Focusable="False"
FontFamily="Courier" FontSize="9" FontWeight="Bold" Margin="0,1,0,0" Padding="0"
VerticalContentAlignment="Bottom" Width="16" Height="16"/>
</DataTemplate>
</Window.Resources>
<Grid>
<TabControl Name="tabMain" ItemsSource="{Binding TabItems,UpdateSourceTrigger=PropertyChanged}" />
</Grid>
In my view model I have the add functionality where a new tab has been added. I need the close button for all these newly added tabs.
public MainViewModel()
{
try
{
Home Item2 = new Home();
TabItems.Add(new TabItem() { Header = "Home", Content = Item2 });
}
catch(Exception ex)
{
MessageBox.Show("Exception "+ex);
}
//Function to add new tabs.
public void AddNewTabs()
{
ChildWindow childContent = new ChildWindow();
TabItem item = new TabItem() { Header = "New Tab", Content = childContent};
item.MouseDoubleClick += new MouseButtonEventHandler(tab_MouseDoubleClick);
TabItems.Add(item);
}
Right now new tabs are being added but without the close button. I have tried giving
item.HeaderTemplate = FindResource("ClosableTabItemTemplate") as DataTemplate;
But it shows error.
Any help would be appreciated.
Thanks in advance.

You could have a look at Dragablz which does this and more.
Disclaimer: this is my library, but it's open source, so enjoy.

You view model must not interact directly with the view in order to respect the Mvvm pattern, meaning that you need to use commands instead of Events, don't use any view related control in your View model logic ..
here a cleaner way to achieve what you're looking for :
First In the view use the TabControl ContentTemplate and ItemTemplate Instead :
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Button Content="Add new tab" Command="{Binding AddNewTabCommand}"></Button>
<TabControl Grid.Row="1" Name="TabMain" ItemsSource="{Binding TabItems,UpdateSourceTrigger=PropertyChanged}" >
<TabControl.ItemTemplate>
<DataTemplate >
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Header}"/>
<Button Content="X" Cursor="Hand" DockPanel.Dock="Right" Focusable="False"
FontFamily="Courier" FontSize="9" FontWeight="Bold" Margin="0,1,0,0" Padding="0"
VerticalContentAlignment="Bottom" Width="16" Height="16" Command="{Binding DataContext.CloseTabCommand,RelativeSource={RelativeSource AncestorType={x:Type Window}}}" CommandParameter="{Binding ElementName=TabMain,Path=SelectedItem}"/>
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<TextBlock Text="{Binding Content}" VerticalAlignment="Center" HorizontalAlignment="Center"></TextBlock>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
Second, In the ViewModel Create a class TabItem that will hold a tab Content and header (customize it as needed), you may want to implement the INotifyPropertyChanged interface if the class reflect any changes to the view,
Third, define the commands to add and romove a TabItem from TabItems ObservableCollection,
here the viewModel Code :
public class TabItem
{
public String Header { get; set; }
public String Content { get; set; }
}
public class MainViewModel : INotifyPropertyChanged
{
private ObservableCollection<TabItem> _tabItems;
public ObservableCollection<TabItem> TabItems
{
get
{
return _tabItems;
}
set
{
if (_tabItems == value)
{
return;
}
_tabItems = value;
OnPropertyChanged();
}
}
private RelayCommand _addNewTabCommand;
public RelayCommand AddNewTabCommand
{
get
{
return _addNewTabCommand
?? (_addNewTabCommand = new RelayCommand(
() =>
{
TabItems.Add(new TabItem()
{
Header = "NewTab",
Content = "NewContent"
});
}));
}
}
private RelayCommand<TabItem> _closeTabCommand;
public RelayCommand<TabItem> CloseTabCommand
{
get
{
return _closeTabCommand
?? (_closeTabCommand = new RelayCommand<TabItem>(
(t) =>
{
TabItems.Remove(t);
}));
}
}
public MainViewModel()
{
TabItems = new ObservableCollection<TabItem>()
{
new TabItem()
{
Header = "Home",
Content = "Home Content"
},
new TabItem()
{
Header = "Header1",
Content = "Content1"
}
};
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
the output :
Ps: My MainWindow View DataContext is set to MainWindowViewModel, and tha's why i am using AncestorType to find the CloseTabCommand

Related

WPF Using TabItem and binding ViewModels

My goal is have each TabItem linked to a specific viewmodel. Furthermore, after transvering through each TabItem, the user input should not be reset. I am finding solutions for this and came across a potential solution but my testing failed me.
I have searched for answers and chose to do the following as it seems it applies the MVVM concept and it looks neat! However, I have a XAML binding error. I tried to replicate Jakob Christensen's answer provided in this link. I tried to debug and I think the issue is with the type of ObservableCollection that is created. It's an object type.
Thank you for helping!
Error
This is the XAML code for my view
<TabControl>
<TabItem DataContext="{Binding TabList[0]}" x:Name="Tab1" Header="Tab1" Margin="-2,-2,-2,2" >
<Grid>
<TextBox x:Name ="EnterNum1" Margin="300,100,300,300" Text="{Binding test1, Mode =TwoWay}"/>
<Button Name="RunBtn1" Command="{Binding Path=RunBtn1, Mode=TwoWay}" Content="RUN" HorizontalAlignment="Right" Width="180" Height="40" FontSize="18"/>
</Grid>
</TabItem>
<TabItem DataContext="{Binding TabList[1]}" x:Name="Tab2" Header="Tab2" >
<Grid>
<TextBox x:Name ="EnterNum2" Margin="300,100,300,300" Text="{Binding test2, Mode =TwoWay}" Grid.Column="1"/>
<Button Name="RunBtn2" Command="{Binding Path=RunBtn2, Mode=TwoWay}" Content="RUN" HorizontalAlignment="Right" Width="180" Height="40" FontSize="18"/>
</Grid>
</TabItem>
</TabControl>
This is the XAML.cs for my view
public ObservableCollection<object> TabList { get; set; }
public ImportData()
{
InitializeComponent();
TabList = new ObservableCollection<object>();
TabList.Add(new SampleViewModel1());
TabList.Add(new SampleViewModel2());
}
You should first set DataContext to TabControl or Window,
then you can bind the TabItem to the viewmodel(TabList) list item
XAML
<TabControl>
<TabItem DataContext="{Binding TabList[0]}" x:Name="Tab1" Header="Tab1" Margin="-2,-2,-2,2" >
<Grid>
<TextBox x:Name ="EnterNum1" Margin="300,100,300,240" Text="{Binding Test1, Mode =TwoWay}"/>
<Button Name="RunBtn1" Command="{Binding Path=RunBtn1, Mode=TwoWay}" Content="RUN" HorizontalAlignment="Right" Width="180" Height="40" FontSize="18"/>
</Grid>
</TabItem>
<TabItem DataContext="{Binding TabList[1]}" x:Name="Tab2" Header="Tab2" >
<Grid>
<TextBox x:Name ="EnterNum2" Margin="300,100,300,240" Text="{Binding Test2, Mode =TwoWay}" Grid.Column="1"/>
<Button Name="RunBtn2" Command="{Binding Path=RunBtn2, Mode=TwoWay}" Content="RUN" HorizontalAlignment="Right" Width="180" Height="40" FontSize="18"/>
</Grid>
</TabItem>
</TabControl>
XAML.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
AllViewModel allViewModel = new AllViewModel();
allViewModel.TabList = new ObservableCollection<object>();
allViewModel.TabList.Add(new SampleViewModel1());
allViewModel.TabList.Add(new SampleViewModel2());
this.DataContext = allViewModel;
}
}
public class AllViewModel : INotifyPropertyChanged
{
private ObservableCollection<object> tabList;
public ObservableCollection<object> TabList
{
get => tabList;
set { tabList = value;RaiseChange("TabList"); }
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaiseChange(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class SampleViewModel1 : INotifyPropertyChanged
{
private string test1 = "test1";
private ICommand runBtn1;
public string Test1
{
get => test1;
set { test1 = value;RaiseChange("Test1"); }
}
public ICommand RunBtn1
{
get => runBtn1;
set { runBtn1 = value;RaiseChange("RunBtn1"); }
}
public SampleViewModel1()
{
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaiseChange(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class SampleViewModel2 : INotifyPropertyChanged
{
private string test2 = "test2";
private ICommand runBtn2;
public string Test2
{
get => test2;
set { test2 = value; RaiseChange("Test2"); }
}
public ICommand RunBtn2
{
get => runBtn2;
set { runBtn2 = value; RaiseChange("RunBtn2"); }
}
public SampleViewModel2()
{
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaiseChange(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
run result

Dynamic ViewModel's bound values becomes null in WPF

I followed few online tutorials to switch between dynamic views from a ListView.
In my MainWindow, I have a ListView in the left pane with ItemsSource of list of sub ViewModels. (Each sub ViewModel implements an Interface)
Each ViewModel have its own View as a DataTemplate.
I'm calling the GenerateReport() method of the selected view from the MainWindow. But the values of selected views becomes null.
Download my source from Github.
Steps to reproduce the issue:
Run the application and type text in the Students Report's Id and Name. (The breakpoints in StudentReportViewModels's properties are properly hitting and values updated.)
Then click Generate Report button. The StudentReportViewModels's properties values becomes null.
How to fix the issue? Please help.
Source:
MainWindow.xaml
<Window.Resources>
<DataTemplate DataType="{x:Type vm:StudentsReportViewModel}">
<view:StudentsReport/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:MarksReportViewModel}">
<view:MarksReport />
</DataTemplate>
</Window.Resources>
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ListView ItemsSource="{Binding Reports}" SelectedItem="{Binding SelectedReport, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<GridSplitter Grid.Row="1" Grid.Column="1" Width="5" ResizeBehavior="PreviousAndNext" HorizontalAlignment="Stretch"/>
<Grid Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="2*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<ContentControl Content="{Binding SelectedReport.ViewModel, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</ScrollViewer>
<Button Content="Generate Report" Grid.Row="2" Margin="5" HorizontalAlignment="Right" Command="{Binding GenerateReportCommand}"/>
</Grid>
</Grid>
MainWindowViewModel:
public class MainWindowViewModel : INotifyPropertyChanged
{
private ICommand _generateReportCommand;
private Report selectedReport;
public Report SelectedReport
{
get
{
return this.selectedReport;
}
set
{
if (value != this.selectedReport)
{
this.selectedReport = value;
NotifyPropertyChanged();
}
}
}
public List<Report> Reports { get; set; }
public ICommand GenerateReportCommand
{
get
{
if (_generateReportCommand == null)
{
_generateReportCommand = new RelayCommand(
p => GenerateReport()
);
}
return _generateReportCommand;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public MainWindowViewModel()
{
Reports = new List<Report>
{
new Report{ Name = "Students Report", ViewModel = new StudentsReportViewModel()},
new Report{ Name = "Marks Report", ViewModel = new MarksReportViewModel()}
};
SelectedReport = Reports[0];
}
public void GenerateReport()
{
SelectedReport.ViewModel.GenerateReport();
}
}
StudentsReport.xaml
<TextBox Height="25" Width="100" Margin="5" Text="{Binding Id, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Height="25" Width="100" Margin="5" Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
StudentsReportViewModel:
public class StudentsReportViewModel : INotifyPropertyChanged, IReportViewModel
{
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private string id;
public string Id
{
get
{
return this.id;
}
set
{
if (value != this.id)
{
this.id = value;
NotifyPropertyChanged();
}
}
}
private string name;
public string Name
{
get
{
return this.name;
}
set
{
if (value != this.name)
{
this.name = value;
NotifyPropertyChanged();
}
}
}
public StudentsReportViewModel()
{
}
public void GenerateReport()
{
System.Windows.MessageBox.Show($"Id = {Id}, Name = {Name}");
}
}
Interface:
public interface IReportViewModel
{
void GenerateReport();
}
Model:
public class Report
{
public string Name { get; set; }
public IReportViewModel ViewModel { get; set; }
}
Your StudentsReport.xaml UserControl is binding to an instance of the StudentsReportViewModel created in the XAML:
<UserControl.DataContext>
<vm:StudentsReportViewModel/>
</UserControl.DataContext>
The Generate Report button however is calling into another instance of the StudentsReportViewModel that is create in the MainWindowVieModel constructor and is stored in the Report class.
Reports = new List<Report>
{
new Report{ Name = "Students Report", ViewModel = new StudentsReportViewModel()},
new Report{ Name = "Marks Report", ViewModel = new MarksReportViewModel()}
};
You need to remove one of these instances so that the UserControl's DataContext is bound to the same view model instance that you are generating the report message from.
I suggest deleting this code from StudentsReport.xaml:
<UserControl.DataContext>
<vm:StudentsReportViewModel/>
</UserControl.DataContext>

WPF Updating DataBinding

So, I tested many answers I found on different topics, but still my WPF app does not update binded data. When I set all Properties before Initializing MainWindow Data are displayed correctly, but I need to select directory, date, etc. before loading the data. Tried to change DataContext in code behind, but IT doesn't work. All the classes used as VieModels have implemented INotifyPropertyChanged interface (but the PropertyChanged values is always null). I'm out of ideas now...
This is XAML code:
<Window x:Class="WpfDataBinding.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:WpfDataBinding"
mc:Ignorable="d"
Title="MainWindow" Height="600" Width="800" Name="Logs">
<Window.DataContext>
<local:CustomDataContexts />
</Window.DataContext>
<Grid Name="Logi">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<local:CustomButton Grid.Column="0" Grid.Row="1" Margin="5" Height="35" Width="100" x:Name="Choose" Text="Wybierz:" ImageSource="Resources/choose.png" Click="CustomButton_Click" />
<local:CustomButton Grid.Column="0" Grid.Row="2" Margin="5" Height="35" Width="100" x:Name="Load" Text="ZaƂaduj:" ImageSource="Resources/load.png" Click="CustomButton_Click" />
<local:CustomButton Grid.Column="0" Grid.Row="3" Margin="5" Height="35" Width="100" x:Name="Search" Text="Szukaj:" ImageSource="Resources/search.png" Click="CustomButton_Click" />
<local:CustomButton Grid.Column="3" Grid.Row="2" Margin="5" Height="80" Width="100" x:Name="Next" Grid.RowSpan="2" Text="Dalej:" ImageSource="Resources/next.png" Click="CustomButton_Click" />
<TabControl DataContext="{Binding TextViewModel}" x:Name="tabControl" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" ItemsSource="{Binding TxtView.Tabs, ElementName=Logs, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}">
<TabControl.ItemTemplate>
<!-- this is the header template-->
<DataTemplate>
<TextBlock Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate >
<!-- this is the body of the TabItem template-->
<DataTemplate>
<ListView ItemsSource="{Binding EntryViewModels}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Entry.Tag}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
<TabControl DataContext="{Binding SingleNode}" x:Name="tabControl2" Grid.Row="0" Grid.Column="2" Grid.ColumnSpan="2" ItemsSource="{Binding Tabs}">
<TabControl.ItemTemplate>
<!-- this is the header template-->
<DataTemplate>
<TextBlock Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<ListView ItemsSource="{Binding Content}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
Code-behind looks like this:
public CustomDataContexts DataContexts { get; set; }
public string Path { get; set; }
public Files Files { get; set; }
public MainWindow()
{
/*Path = #"C:\Users\Slawek\Desktop\Logs\logi";
Files = new Files(Path);
Files.NarrowFiles(false, DateTime.MinValue);
var entry = new EntryCollection(Files.SelectedFiles[1], Files.SelectedFiles, null);
TxtView = new TxtViewModel(new List<TxtTabItem>(new[] { new TxtTabItem(entry) }));*/
InitializeComponent();
}
private void CustomButton_Click(object sender, RoutedEventArgs e)
{
var fe = (FrameworkElement) sender;
switch (fe.Name)
{
case "Choose":
var g = new FolderBrowserDialog();
if (g.ShowDialog() == System.Windows.Forms.DialogResult.OK)
Path = g.SelectedPath;
break;
case "Load":
DataContexts = new CustomDataContexts();
Files = new Files(Path);
Files.NarrowFiles(false, DateTime.MinValue);
var entry = new EntryCollection(Files.SelectedFiles[1], Files.SelectedFiles, null);
DataContexts.TextViewModel = new TxtViewModel(new List<TxtTabItem>(new[] { new TxtTabItem(entry) }));
break;
case "Search":
break;
case "Next":
break;
}
}
CustomDataContexts class:
public class CustomDataContexts : INotifyPropertyChanged
{
private TxtViewModel textViewModel;
public XmlViewModel SingleNode { get; set; }
public TxtViewModel TextViewModel
{
get { return textViewModel; }
set { OnPropertyChanged("TextViewModel"); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
EntryViewModel:
public class EntryViewModel : INotifyPropertyChanged
{
private SingleEntry entry;
public SingleEntry Entry
{
get { return entry; }
set
{
entry = value;
OnPropertyChanged("Entry");
}
}
public EntryViewModel(SingleEntry entry)
{
Entry = entry;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
TxtViewModel:
public class TxtViewModel :INotifyPropertyChanged
{
private ObservableCollection <TxtTabItem> tabs;
public ObservableCollection<TxtTabItem> Tabs
{
get { return tabs; }
set
{
tabs = value;
OnPropertyChanged("Tabs");
}
}
public TxtViewModel(List<TxtTabItem> items)
{
Tabs = new ObservableCollection <TxtTabItem>();
foreach (var txtTabItem in items)
{
Tabs.Add(txtTabItem);
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
And finally TxtTabItem class:
public class TxtTabItem : INotifyPropertyChanged
{
public string Header { get; set; }
public ObservableCollection<EntryViewModel> EntryViewModels { get; set; }
public TxtTabItem(EntryCollection collection)
{
Header = collection.Date.ToShortDateString();
EntryViewModels = new ObservableCollection <EntryViewModel>();
foreach (var entry in collection.Entries)
{
EntryViewModels.Add(new EntryViewModel(entry));
}
OnPropertyChanged("EntryViewModels");
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
I will be very grateful for any suggestions of how to make this code work. I'm pretty new to WPF and still don't know it well enough.
The framework is there, but generally to intialize controls to changes one binds to the ItemsSource property instead of directly to the DataContext.
In general the DataContext is something that provides information about the current item, but when it is null the parent's data context is used up until the page's data context.
The reason for that is a data context will hold a large class of properties and the binding will look (reflect/reflection) at the DataContext for that named property. So by using an ItemsSource one can bind to a specific set of items, while still having a DataContext full of information to allow other items on the control to be bound to other specific properties.
Under MVVM where the VM or view model is that class of properties as mentioned, set the page's data context to that VM and then bind to individual properties on the different controls you have from the page's datacontext.
so
<TabControl DataContext="{Binding TextViewModel}" x:Name="tabControl"
becomes
<TabControl ItemsSource="{Binding TextViewModel}" x:Name="tabControl"
once you set the page's data context to TxtViewModel.
I provide a binding/VM example on my blog article Xaml: ViewModel Main Page Instantiation and Loading Strategy for Easier Binding.

Button doesn't work in FlipView with DataTemplates - WinRT

I'm using Flipview and a DataTemplateSelector to determine at runtime which DataTemplate to apply to show items in my control.
I have two DataTemplate's, one is static and the second can be used by a undetermined number of items.
The problem is that the button does nothing. I've used a breakpoint in SaveCommand but when I click the button, it doesn't break.
XAML
<Page.Resources>
<DataTemplate x:Key="FirstDataTemplate">
<Grid>
<TextBlock Text="{Binding Content}" Margin="10,0,18,18"></TextBlock>
</Grid>
</DataTemplate>
<DataTemplate x:Key="SecondDataTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"></ColumnDefinition>
<ColumnDefinition Width="auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding Url}"></TextBox>
<Button Grid.Column="1" Name="SendButton"
Style="{StaticResource ImageButtonStyle}"
Command="{Binding Path=SaveCommand}"
HorizontalAlignment="Center">
<Grid>
<Image Source="ms-appx:///Skins/Images/buton.png" Stretch="None" />
<TextBlock Text="CLICK ME" VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="White"/>
</Grid>
</Button>
</Grid>
</DataTemplate>
<local:MyDataTemplateSelector x:Key="MyDataTemplateSelector"
FirstTextTemplate="{StaticResource FirstDataTemplate}"
SecondTextTemplate="{StaticResource SecondDataTemplate}">
</local:MyDataTemplateSelector>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<FlipView x:Name="itemGridView" ItemTemplateSelector="{StaticResource MyDataTemplateSelector}"
Margin="265,220,284,162">
</FlipView>
</Grid>
Code-Behind
public sealed partial class FlipViewDemo : Page
{
public FlipViewDemo()
{
this.InitializeComponent();
var items = new List<BaseClass>();
items.Add(new FirstItem
{
Content="This is a test - Content"
});
for (int i = 0; i < 18; i++)
{
items.Add(new SecondItem
{
Url = "http://www.google.com/ " + i.ToString()
});
}
itemGridView.ItemsSource = items;
}
}
public class BaseClass
{
}
public class FirstItem : BaseClass
{
public string Content { get; set; }
}
public class SecondItem : BaseClass
{
public string Url { get; set; }
}
public class MyDataTemplateSelector : DataTemplateSelector
{
public DataTemplate FirstTextTemplate { get; set; }
public DataTemplate SecondTextTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
if (item is FirstItem)
return FirstTextTemplate;
if (item is SecondItem)
return SecondTextTemplate;
return base.SelectTemplateCore(item, container);
}
}
ViewModel
public class FisaObsViewModel : ViewModelBase
{
private RelayCommand saveCommand;
public FisaObsViewModel()
{
}
public RelayCommand SaveCommand
{
get
{
return saveCommand ?? (saveCommand = new RelayCommand(
async () =>
{
try
{
MessageDialog dlg = new MessageDialog("Message");
await dlg.ShowAsync();
}
catch (Exception)
{
throw;
}
}));
}
}
}
Some of the links that I've checked:
http://www.mutzl.com/tag/mvvm-light/
http://www.codeproject.com/Articles/126249/MVVM-Pattern-in-WPF-A-Simple-Tutorial-for-Absolute
http://social.technet.microsoft.com/wiki/contents/articles/18199.event-handling-in-an-mvvm-wpf-application.aspx
The problem is that the DataTemplate is referencing your Model, not your ViewModel. So your command binding is trying to find the command on the Model.
You need to change your binding source to whatever element has it's DataContext set to the ViewModel.
{Binding DataContext.SaveCommand, RelativeSource={RelativeSource AncestorType=Page}}
Or
Give your Page a Name, and use the following binding:
{Binding DataContext.SaveCommand, ElementName=myPageName}

Failing To Bind Dynamically Generated Buttons & Textboxes in WPF

I was working on dynamic generation of labels, buttons and Textbox in my WPF application. Well I was successful in dynamically creating them but I am facing one major issue in it.
Xaml:
<ListBox x:Name="myViewChannelList" HorizontalAlignment="Stretch" Height="Auto" ItemsSource="{Binding}" Margin="0" VerticalAlignment="Stretch" Width="Auto">
<ListBox.ItemTemplate>
<DataTemplate >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="170" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="{Binding Path=ChanelName}" Margin="50,20,0,0"></Label>
<Grid Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding Path=VoltageText}" Height="25" Width="50" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,0,0,0" />
<Button Grid.Column="1" Content="Set" Height="25" Command="{Binding ElementName=myViewChannelList, Path=DataContext.SetCommand}" Width="50" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,0,0,0" ></Button>
</Grid>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Model Class:
private string _ChanelName = "";
public String ChanelName
{
get
{
return _ChanelName;
}
set
{
if (value != _ChanelName)
{
_ChanelName = value;
OnPropertyChanged("ChanelName");
}
}
}
// Constructor
public VoltageModel(string ChanelName)
{
this.ChanelName = ChanelName;
}
public override string ToString()
{
return _ChanelName;
}
ViewModel Class:
class ChannelList : ObservableCollection<VoltageModel>, INotifyPropertyChanged
{
private string _VoltageText;
public string VoltageText
{
get { return _VoltageText; }
set
{
_VoltageText = value;
OnPropertyChanged("VoltageText");
}
}
// Method gets called when Set Button Is Clicked
public void SetCommandExecuted()
{
string val = VoltageText;
}
//Notify Property Changed members are present
}
Xaml.cs Class:
ChannelList myChanels = new ChannelList();
public VoltageView() // Constructor
{
InitializeComponent();
myChanels.Add(new VoltageModel("VDD__Main"));
myChanels.Add(new VoltageModel("VDD__IO__AUD"));
myChanels.Add(new VoltageModel("VDD__CODEC__AUD"));
myViewChannelList.DataContext = myChanels;
}
This gives me 3 Labels(Content as above), 3 textboxes and 3 buttons when I run the application.
Now when I enter the value inside the textbox it shows null on button click when I put a breakpoint in SetCommandExecuted(). Most importantly any of the 4 button I click generates the event. I want the first textbox and first button to be in sync(bind), 2nd textbx and 2nd button to be in sync and so on. Basically each control must be in sync with the other control in a row. It should not effect the other rows. Is it possible???
Here is the solution to your question. As general practice you want to avoid all logic, building your data, etc. in the code behind. All the business logic should be in the view model which will make it easier to unit test.
Here is the view
.xaml
<StackPanel>
<ListBox HorizontalAlignment="Stretch"
Height="Auto"
ItemsSource="{Binding VoltageCollection}"
VerticalAlignment="Stretch"
Width="Auto">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Label Width="100"
Content="{Binding ChannelName}" />
<TextBox Width="100"
Text="{Binding VoltageText}" />
<Button Margin="10,0,0,0"
Content="Set"
Command="{Binding VoltageCommand}"
CommandParameter="{Binding VoltageText}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
Here is the code behind
.xaml.cs
private ChannelListViewModel m_voltageViewModel;
public MainWindow()
{
InitializeComponent();
m_voltageViewModel = new ChannelListViewModel();
m_voltageViewModel.Initialize();
DataContext = m_voltageViewModel;
}
Here is the Model: VoltageModel
public class VoltageModel : INotifyPropertyChanged
{
public string ChannelName { get; set; }
private string m_voltageText;
public string VoltageText
{
get { return m_voltageText; }
set
{
m_voltageText = value;
OnPropertyChanged("VoltageText");
}
}
public ICommand VoltageCommand { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Here is the ViewModel: ChannelListViewModel
public class ChannelListViewModel
{
private ICommand m_voltageCommand;
public ChannelListViewModel()
{
m_voltageCommand = new DelegateCommand(x => SetCommandExecute(x));
}
public void Initialize()
{
VoltageCollection = new ObservableCollection<VoltageModel> { new VoltageModel() { ChannelName = "VDD__Main", VoltageText = String.Empty, VoltageCommand = m_voltageCommand },
new VoltageModel() { ChannelName = "VDD__IO__AUD", VoltageText = String.Empty, VoltageCommand = m_voltageCommand },
new VoltageModel() { ChannelName = "VDD__CODEC__AUD", VoltageText = String.Empty, VoltageCommand = m_voltageCommand }};
}
public ObservableCollection<VoltageModel> VoltageCollection { get; set; }
public void SetCommandExecute(object voltageText)
{
Debug.WriteLine(voltageText);
}
}
Finally simple DelegateCommand class DelegateCommand
public class DelegateCommand : ICommand
{
Action<object> m_executeDelegate;
public DelegateCommand(Action<object> executeDelegate)
{
m_executeDelegate = executeDelegate;
}
public void Execute(object parameter)
{
m_executeDelegate(parameter);
}
public bool CanExecute(object parameter) { return true; }
public event EventHandler CanExecuteChanged;
}
i didn't get much into what was wrong since i recognized 2 things that were generally very wrong ,and they might be the problem for unexpected behavior on your part .
the first : your DataTemplate places your control one on top of the other .
fix :
<DataTemplate >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="{Binding Path=ChanelName}" />
<TextBox Grid.Column="1" Text="{Binding Path=VoltageText}" />
<Button Grid.Column="2" Command="{Binding ElementName=myViewChannelList, Path=DataContext.SetCommand}" />
</Grid>
</DataTemplate>
the second : your Properties are set after PropertyChanged event was risen so they would not be updated until the next time you input a value.
fix :
private T _property;
public T Property
{
get { return _property; }
set
{
_property = value;
OnPropertyChanged("Property");
}
}
make these fixes and edit your post if you still have issues post a comment under my answer.

Categories