Getting parent of new tab after adding to bound TabControl (mvvm) - c#

I'm adding a close button to my tabs using the following guide:
http://www.codeproject.com/Articles/84213/How-to-add-a-Close-button-to-a-WPF-TabItem
This has become a problem because the event uses the 'parent' of the added tab to remove that tab from the tabcontrol. I'm binding the tab control using mvvm, so the parent property is apparently not being set and giving me a null reference exception for the parent when the event tries to remove from it.
Here's the binding so you get the idea:
<TabControl Name="tabControl" Margin="0,22,0.2,-5.2" ItemsSource="{Binding Tabs}" Background="#FF4C76B2"/>
Heres where the tabs are being added.
private void AddTab(object tabName)
{
ClosableTab newTab = new ClosableTab();
newTab.Title = "title?";
//newTab.Header = tabName;
TextBox test = new TextBox();
test.Text = "CONTENT (" + tabName + ") GOES HERE";
newTab.Content = test;
Tabs.Add(newTab);
OnPropertyChanged("Tabs");
}
Here is the event where the null reference is taking place:
void button_close_Click(object sender, RoutedEventArgs e)
{
((TabControl)this.Parent).Items.Remove(this);
}
As I see it there are two options:
try to find another way to remove the tab (without the parent
property)
try to find a way to somehow set the parent property (which cant be
done directly, it throws a compiler error)

That doesn't sound like MVVM to me. We work with data, not UI elements. We work with collections of classes that contain all of the properties required to fulfil some requirement and data bind those properties to the UI controls in DataTemplates. In this way, we add UI controls by adding data items into these collections and let the wonderful WPF templating system take care of the UI.
For example, you have a TabControl that we want to add or remove TabItems from... in a proper MVVM way. First, we need a collection of items that can represent each TabItem:
public static DependencyProperty ItemsProperty = DependencyProperty.Register("Items", typeof(ObservableCollection<string>), typeof(TestView));
public ObservableCollection<string> Items
{
get { return (ObservableCollection<string>)GetValue(ItemsProperty); }
set { SetValue(ItemsProperty, value); }
}
I'm just using a DependencyProperty because I knocked this up in a UserControl and I'm just using a collection of strings for simplicity. You'll need to create a class that contains all of the data required for the whole TabItem content. Next, let's see the TabControl:
<TabControl ItemsSource="{Binding Items}" ItemTemplate="{StaticResource ItemTemplate}" />
We data bind the collection to the TabControl.ItemsSource property and we set the TabControl.ItemTemplate to a Resource named ItemTemplate. Let's see that now:
xmlns:System="clr-namespace:System;assembly=mscorlib"
...
<DataTemplate x:Key="ItemTemplate" DataType="{x:Type System:String}">
<TabItem Header="{Binding}" />
</DataTemplate>
This DataTemplate defines what each item in our collection will look like. For simplicity's sake, our strings are just data bound to the TabItem.Header property. This means that for each item we add into the collection, we'll now get a new TabItem with its Header property set to the value of the string:
Items.Add("Tab 1");
Items.Add("Tab 2");
Items.Add("Tab 3");
Note that I included the System XML Namespace Prefix for completeness, but you won't need that because your DataType will be your own custom class. You'll need more DataTemplates too. For example, if your custom class had a Header property and a Content property, which was another custom class, let's say called Content, that contained all of the properties for the TabItem.Content property, you could do this:
<DataTemplate x:Key="ItemTemplate" DataType="{x:Type YourPrefix:YourClass}">
<TabItem Header="{Binding Header}" Content="{Binding Content}" />
</DataTemplate>
<DataTemplate DataType="{x:Type YourPrefix:Content}">
<YourPrefix:SomeUserControl DataContext="{Binding}" />
</DataTemplate>
So this would give you TabItems with Headers set and Content that comes from SomeUserControl which you could design. You don't need to use UserControls, you could just add more UI controls to either DataTemplate. But you will need to add more controls somewhere... and more classes and properties, always remembering to correctly implement the essential INotifyPropertyChanged interface.
And finally, to answer your question in the proper MVVM way... to remove a TabItem, you simply remove the item that relates to that TabItem from the collection. Simple... or it would have been if you really had been using MVVM like you claim. It's really worth learning MVVM properly as you'll soon see the benefits. I'll leave you to find your own tutorials as there are many to chose from.
UPDATE >>>
Your event handling is still not so MVVM... you don't need to pass a reference of any view model anywhere. The MVVM way is to use commands in the view model. In particular, you should investigate the RelayCommand. I have my own version, but these commands enable us to perform actions from data bound Buttons and other UI controls using methods or inline delegates in the view model (where action and canExecute in this example are the CommandParameter values):
<Button Content="Close Tab" Command="{Binding CloseTabCommand}"
CommandParameter="{Binding}" />
...
public ICommand CloseTabCommand
{
get { return new ActionCommand(action => Items.Remove(action),
canExecute => canExecute != null && Items.Contains(canExecute)); }
}
So whatever view model has your Tabs collection should have an AddTabCommand and a CloseTabCommand that add and remove items from the Tabs collection. But just to be clear, for this to work properly, your ClosableTab class should be a data class and not a UI control class. Use a DataTemplate to specify it if it is a UI control.
You can find out about the RelayCommand from this article on MSDN.

Related

TreeViewItem Command binding

I need to click on an TreeViewItem and open an dialog window with the data of that TreeViewItem, later based on that data I will run another command.
My actual problem is: I can't click on it because treeviewitem doesn't have the command property.
My scenario: I have 2 Models with 2 properties that will be used to create my TreeViewItems. On my ViewModel I create them, and organize them inside each other based on their properties and then store them inside One Collection.
Here's my xaml to bind the elements:
<TreeView ItemsSource="{Binding Local}">
<TreeView.DataContext>
<data:ItemViewModel/>
</TreeView.DataContext>
</Treeview>
//In my "Local" property I have 3 TreeViewItems with other items inside them which I want to execute the commands
I couldn't find a way to create a datatemplate for that specific scenario. Even tried to create a datatemplate with a Hyperlink (thought it would be a temporary solution) inside it, but would not execute any command.
MVVM pattern is to use one of the many "Event to Command" implementations out there. You basically bind the "Event to Command" object to the Click event and then a command in your VM gets bound to the "Event to Command" object and it gets mapped behind the scenes for you and handles all the enabled / disabled stuff.
You can see an example of one of the implementations here:
WPF Binding UI events to commands in ViewModel
You should be binding to a collection whose objects have a collection as a public property and templating by type into whatever you want to see in each treeviewitem.
Like this sample:
https://learn.microsoft.com/en-us/dotnet/api/system.windows.hierarchicaldatatemplate?view=netframework-4.7.2
Technically, you could have a button whose template was a textblock or something and that would then have the behaviour of a button such as click and command.
But I'd be more likely to use an inputbinding.
Here's an example:
<DataTemplate DataType="{x:Type local:LineTerrainVM}">
<Grid Background="Transparent">
<Grid.InputBindings>
<MouseBinding MouseAction="RightClick" Command="{Binding FixLineCommand}"/>
</Grid.InputBindings>
You can give that a commandparameter="{Binding .}" and it'll pass the viewmodel as a parameter.
You could also use relativesource to the datacontext of the treeview to get at a parent viewmodel and define a command in that to do your stuff.
Since that stuff you want to do is a view responsibility you could rely on routed events without "breaking" mvvm. A click in any treeviewitem could be handled at the treeview level and use the originalsource to get to the treeviewitem clicked. Then grab it's datacontext for the viewmodel of whatever that is.
Rough idea:
<TreeView Name="tv" ItemsSource="{Binding Families}" FrameworkElement.PreviewMouseLeftButtonDown="Tv_PreviewMouseLeftButtonDown"
And the handler:
private void Tv_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var vm = ((FrameworkElement)e.OriginalSource).DataContext;
// do something with viewmodel
}
You could then do something like new up your dialog window, set it's datacontext to that viewmodel you just got and showdialog it.

How to properly bind to element property in WPF

I have little problem with binding. I have stackpanel in my xaml which has some elements in children collection. Second i have textblock which shows count of elements in stackpanel. It is done by binding that way
<TextBlock Text="{Binding Children.Count, ElementName=CommentsContainer, Mode=OneWay, StringFormat=({0})}" />
<StackPanel x:Name="CommentsContainer"></StackPanel>
It works fine for first-time but if is something added to stackpanel children collection dinamically textblock text is not updated. I mean that collection count does not implement inotifypropertychange, but how do something like this properly?
You asked "how [to] do something like this properly". The WPF way of doing it would be to have a collection of items implemented as a property on your Window or ViewModel or whatever, then bind an ItemsControl to that collection.
For example, if you had a collection of strings:
public ObservableCollection<string> MyItems { get; private set; }
// elsewhere in the same class...
MyItems = new ObservableCollection<string>();
MyItems.Add("first");
MyItems.Add("second");
MyItems.Add("etc");
ObservableCollection<T> is a good collection class to use for WPF as notification of any changes made to the collection (such as adding or removing items) will be pushed to any observers of the collection (such as WPF's binding system).
To see these items in your View (eg. Window, UserControl, etc), you would use a control that can display a list of items (one derived from ItemsControl) and bind that control to the list property, like so:
<Window ... >
<StackPanel>
<ItemsControl ItemsSource="{Binding MyItems}" />
<TextBlock Text="{Binding MyItems.Count}" />
</StackPanel>
</Window>
ObservableCollection<T> does implement INotifyPropertyChanged so the Count property will always reflect the actual number of items in the list.
You don't have to have a list of strings of course, they could be any kind of object. Similarly, you don't have to use an ItemsControl but could use something like a ListBox or ListView instead (they both derive from that base control class). Furthermore, you might want to look into data templating as this can be used to change the visual appearance of the items in the ItemsControl.

How do I programatically change tab names in a WPF TabControl?

All,
I have searched extensively for the solution here but I have a feeling my problem stems from a basic lack of knowledge about WPF. I am new to it and have thus far hacked and Googled my way through as best I can.
Basically, I have a Ribbon dynamically interacting with a TabControl. The Ribbon tabs select a category of items, the MenuItems in the RibbonGroups then choose an item within the category. Upon clicking an item, the tabs on the TabControl need to dynamically change. Whether that is just the Headers, the tabs themselves, or the entire TabControl is fine with me. Thus far, upon clicking a MenuItem on the inside one of the RibbonGroups, I attempt to just set the Header text equal to "blah" for each tab on the TabControl. Then the Header object throws a null pointer. This is what happens whether I set the Header, the Tabs, or the TabControl itself.
Why?!?!?!?
...and how in the world do I fix it???
Thanks!
WPF is designed with data/UI separation in mind. One of the reasons you're having trouble finding a solution is because what you're trying to do is a no-no; instead of programmatically changing the UI's header text, you should be changing the underlying data instead, and allowing the WPF plumbing to update how the data is displayed.
A WPF tab control can literally contain any type of object; you could fill it with integers or strings or FooBars or whatever. There's no guarantee that any of these objects will define a Header property, and it's up to the developer to configure data bindings or templates which instruct the TabControl just how a FooBar or a whatever should be displayed.
In an ideal WPF application which adheres to the MVVM design pattern, you might have your TabControl bound to a collection of view models which each define a HeaderText property. Your view models would implement the INotifyPropertyChanged interface so that when the HeaderText property was changed on the view model then the UI would get updated.
Having said all that, if you've got an existing application it may be unrealistic to rewrite it from scratch using a different design pattern, and MVVM is not easily added on to an existing code base. If you're working with simple Designer generated UI without using any data binding, then the following code does what you ask. Sometimes.
foreach(TabItem item in tabControl.Items)
item.Header = "blah";
... but as I said before, there's no guarantee that a WPF TabControl's Items collection will contain items of type TabItem, so this code is not safe.
While RogerN's answer is probably a better answer, here is a code sample that changes the text that appears on a tab:
XAML:
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TabControl Name="MyTabControl">
<TabItem Header="Tab One">
<TextBlock Text="This is tab #1." />
</TabItem>
<TabItem Header="Tab Two">
<TextBlock Text="This is tab #2." />
</TabItem>
<TabItem Header="Tab Three">
<TextBlock Text="This is tab #3." />
</TabItem>
</TabControl>
<Button Grid.Row="1" Content="Change Tab" Name="ChangeButton" Click="ChangeButton_Click" />
</Grid>
Code behind:
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
}
private void ChangeButton_Click(object sender, RoutedEventArgs e) {
((TabItem)MyTabControl.Items[0]).Header = "Changed!";
}
}
Try binding it to a list in the code like so:
private List<TabItem> TabItems = new List<TabItem>()
{
"Item1",
"Item2",
"Item3"
};
tabcontrol1.ItemSource = TabItems;
Then rebind it any time you want to change the items in the tabcontrol. This way you can dynamically change names and add more tab items. In doing this you'll have to programmatically add controls using the TabItem.Content property.

how to create binding in code when the datatemplate is declared in xaml

i have a datatemplate declared in xaml.
for e.g.
<DataTemplate x:Key="TestTemplate">
<StackPanel>
<TextBox Name="txtBox" Visibility="Visible"></TextBox>
</StackPanel>
</DataTemplate>
I wish to set the binding for txtBox in code behind before the element is generated because i have different binding paths for different elements that get generated
I can get the template in the code behind as :
DataTemplate tmplt = FindResource("TestTemplate") as DataTemplate;
but i am not sure what to do next. How to get the the txtBox reference to set the binding.
We have to remember one thing that Templates are not instantiated UI controls. They are streamed obejcts in XAML and are shared between UI elements. So if you edit a dataTemplate and change its stucture (by adding, editing, deleting an element under the template) it would change the one data template which is shared among controls. Thus other elements using that template will also be affected by the change.
Now lets address your issue of adding a dynamic biding to a textbox. You say each generated textbox will have different binding paths. So this definitely does NOT call for changing the data template itself!
You will have to access the text box and add dynamic bindings to it AFTER the textbox's is generated.
I see that your binding differs based on your "situation", so why cant you use TemplateSelector? Template selector will decide which data template (having one specific binding applied to the TetxBox) at runtime.
The first part of answer - is FindName() method.
example:
DataTemplate tmplt = FindResource("TestTemplate") as DataTemplate;
TextBox my = (TextBox)tmplt.FindName("txtBox");
try out this, it should help to get access to TextBox control. I think that you know how to bind to. If you want your DataBinding behave different way, use MultiBinding and Converter.
EDIT
public class GeneralObject
{
private object someObject;
public GeneralObject(object initObject)
{
this.someObject = initObject;
}
//If you want to bind to some text, for example
public string Text
{
get
{
//I think you know which objects are coming as input
if (this.someObject is SpecialClass1)
return ((SpecialClass1)this.someObject).SpecialClass1TextProperty;
if (this.someObject is SpecialClass2)
return ((SpecialClass2)this.someObject).SpecialClass2TextProperty;
//and so on.
}
}
}
EDIT 2
One more possible way
So I remember, that WPF have ContentControl!
<ContentControl Content="{Binding Path=CurrentObject}"/>
But in this case you have to create number of DataTemplate's, every Template for one class.
<DataTemplate DataType="{x:Type local:SpecialClass1}">
...
</DataTemplate>
<DataTemplate DataType="{x:Type local:SpecialClass2}">
...
</DataTemplate>
<!--and so on-->
WPF resolve DataTypes of ContentControl.Content property, and put to the ContentControl right DataTemplate.

UI design using MVVM pattern

I'm trying to choose the best way to implement this UI in MVVM manner. I'm new to WPF (like 2 month's) but I have huge WinForms experience.
The ListBox here act's like a TabControl (so it switches the view to the right), and contains basically the Type of item's displayed in tables. All UI is dynamic (ListBox items, TabItems and Columns are determined during run-time). The application is targeting WPF and Silverlight.
Classes we need for ViewModel:
public abstract class ViewModel : INotifyPropertyChanged {}
public abstract class ContainerViewModel : ViewModel
{
public IList<ViewModel> Workspaces {get;set;}
public ViewModel ActiveWorkspace {get;set;}
}
public class ListViewModel<TItem> where TItem : class
{
public IList<TItem> ItemList { get; set; }
public TItem ActiveItem { get; set; }
public IList<TItem> SelectedItems { get; set; }
}
public class TableViewModel<TItem> : ListViewModel<TItem> where TItem : class
{
public Ilist<ColumnDescription> ColumnList { get; set; }
}
Now the question is how to wire this to View.
There are 2 base approaches I can see here:
With XAML: due to lack of Generics support in XAML, I will lose strong typing.
Without XAML: I can reuse same ListView<T> : UserControl.
Next, how to wire data, I see 3 methods here (with XAML or without doesn't matter here). As there is no simple DataBinding to DataGrid's Columns or TabControl's TabItems the methods I see, are:
Use DataBinding with IValueConverter: I think this will not work with WPF|Silverlight out of the box control's, as some properties I need are read-only or unbindable in duplex way. (I'm not sure about this, but I feel like it will not work).
Use manual logic by subscribing to INotifyPropertyChanged in View: ViewModel.PropertyChanged+= ....ViewModel.ColumnList.CollectionChanged+= ....
Use custom controll's that support this binding: Code by myself or find 3d party controls that support this binding's (I don't like this option, my WPF skill is too low to code this myself, and I doubt I will find free controls)
Update: 28.02.2011
Things get worser and worser, I decided to use TreeView instead of ListBox, and it was a nightmare. As you probably guess TreeView.SelectedItems is a readonly property so no data binding for it. Ummm all right, let's do it the old way and subscribe to event's to sync view with viewmodel. At this point a suddenly discovered that DisplayMemberPath does nothing for TreeView (ummmm all right let's make it old way ToString()). Then in View's method I try to sync ViewModel.SelectedItem with TreeView's:
private void UpdateTreeViewSelectedItem()
{
//uiCategorySelector.SelectedItem = ReadOnly....
//((TreeViewItem) uiCategorySelector.Items[uiCategorySelector.Items.IndexOf(Model.ActiveCategory)]).IsSelected = true;
// Will not work Items's are not TreeViewItem but Category object......
//((TreeViewItem) uiCategorySelector.ItemContainerGenerator.ContainerFromItem(Model.ActiveCategory)).IsSelected = true;
//Doesn't work too.... NULL // Changind DataContext=Model and Model = new MainViewModel line order doesn't matter.
//Allright.. figure this out later...
}
And none of methods I was able to think of worked....
And here is the link to my sample project demonstrating Control Library Hell with MVVM: http://cid-b73623db14413608.office.live.com/self.aspx/.Public/MVVMDemo.zip
Maciek's answer is actually even more complicated than it needs to be. You don't need template selectors at all. To create a heterogeneous tab control:
Create a view model class for each type of view that you want to appear as tab items. Make sure each class implements a Text property that contains the text that you want to appear in the tab for its item.
Create a DataTemplate for each view model class, with DataType set to the class's type, and put the template in the resource dictionary.
Populate a collection with instances of your view models.
Create a TabControl and bind its ItemsSource to this collection, and add an ItemTemplate that displays the Text property for each item.
Here's an example that doesn't use view models (and that doesn't implement a Text property either, because the objects I'm binding to are simple CLR types), but shows how template selection works in this context:
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:coll="clr-namespace:System.Collections;assembly=mscorlib">
<DockPanel>
<DockPanel.Resources>
<coll:ArrayList x:Key="Data">
<sys:String>This is a string.</sys:String>
<sys:Int32>12345</sys:Int32>
<sys:Decimal>23456.78</sys:Decimal>
</coll:ArrayList>
<DataTemplate DataType="{x:Type sys:String}">
<TextBlock Text="{Binding}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type sys:Int32}">
<StackPanel Orientation="Horizontal">
<TextBlock>This is an Int32:</TextBlock>
<TextBlock Text="{Binding}"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type sys:Decimal}">
<StackPanel Orientation="Horizontal">
<TextBlock>This is a Decimal: </TextBlock>
<TextBlock Text="{Binding}"/>
</StackPanel>
</DataTemplate>
</DockPanel.Resources>
<TabControl ItemsSource="{StaticResource Data}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
</DockPanel>
</Page>
Of course in a real MVVM application those DataTemplates would use UserControls to map each type to its view:
<DataTemplate DataType="{x:Type my:ViewModel}">
<my:View DataContext="{Binding}"/>
</DataTemplate>
Maciek and Robert already gave you some ideas on how to implement this.
For the specifics of binding the columns of the DataGrid I strongly recommend Meleak's answer to that question.
Similar to that you can use attached properties (or Behaviors) and still maintain a clean ViewModel in MVVM.
I know the learning curve for WPF is quite steep and you're struggling already. I also know that the following suggestion doesn't help that and even makes that curve steeper. But your scenario is complex enough that I'd recommend to use PRISM.
I wrote an article and a sample application with source code available, where I discuss and show the problems I have mentioned here and how to solve them.
http://alexburtsev.wordpress.com/2011/03/05/mvvm-pattern-in-silverlight-and-wpf/
In order to connect your ViewModel to your View you need to assign the View's DataContext. This is normally done in the View's Constructor.
public View()
{
DataContext = new ViewModel();
}
If you'd like to see your view model's effect at design time, you need to declare it in XAML, in the View's resources, assign a key to it, and then set the target's DataContext via a StaticResource.
<UserControl
xmlns:vm="clr-namespace:MyViewModels
>
<UserControl.Resources>
<vm:MyViewModel x:Key="MyVM"/>
</UserControl.Resources>
<MyControl DataContext={StaticResource MyVM}/>
</UserControl>
(The above is to demonstrate the design-time trick works)
Since you're dealing with a scenario that includes a container such as the TabControl I'd advocate considering the following things :
Hold your child ViewModels in a Property of type ObservableCollection
Bind the TabControls ItemsSource to that property
Create a new View that derives from TabItem
Use a template selector to automatically pick the type of the view based on the type of the view model.
Add IDisposable to yoour child ViewModels and create functionality to close the views.
Hope that helps a bit, if you have further questions let me know.

Categories

Resources