I am doing a WPF application with a TabControl. At the beginning I had a TabControl bound to ObservableCollection of TabBase items, where TabBase is a base class for tab viewmodel:
<TabControl
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Tabs}"
ItemTemplate="{StaticResource ClosableTabTemplate}"
...
public ObservableCollection<TabBase> Tabs { get; private set; }
...
public abstract class TabBase : ViewModelBase
...
public abstract class ViewModelBase : INotifyPropertyChanged
{
public virtual string DisplayName { get; protected set; }
...
<DataTemplate x:Key="ClosableTabTemplate">
<DockPanel Width="120">
<Button
Command="{Binding Path=CmdClose}"
Content="X"
/>
<ContentPresenter
Content="{Binding Path=DisplayName}">
</ContentPresenter>
</DockPanel>
</DataTemplate>
But I've faced with an issue when I switch tabs it looks like current tab is being created each time, even if it was already opened before. Searching thru StackOverflow I've found the solution here with reference to here. I've replaced using of declarative ItemsSource with dynamic creation of tabs from code. Tabs switching performance issue was resolved, but tab headers have lost link to template, so instead of tab header with caption and close button I see just a little tab header without anything. Playing a bit with tab creation code, I was able to restore tab size and close button, but without binding - there is no caption and close button doesn't work (5 lines with item.Header restored original tab size):
private void AddTabItem(TabBase view)
{
TabItem item = new TabItem();
item.DataContext = view;
item.Content = new ContentControl();
(item.Content as ContentControl).Focusable = false;
(item.Content as ContentControl).SetBinding(ContentControl.ContentProperty, new Binding());
item.Header = new ContentControl();
(item.Header as ContentControl).DataContext = view;
(item.Header as ContentControl).Focusable = false;
(item.Header as ContentControl).SetBinding(ContentControl.ContentProperty, new Binding());
item.HeaderTemplate = (DataTemplate)FindResource("ClosableTabTemplate");
tabControl.Items.Add(item);
}
The question is, how can I make ItemTemplate working for TabControl without ItemsSource binding?
When you explicitly set your item.Header to a ContentControl, the HeaderTemplate is now using that object as its DataContext. Normally, the Header property would get your ViewModel and a ContentPresenter would take that (non-Visual) object and apply the HeaderTemplate to it. You've now pushed your ViewModel down a level in the hierarchy so the template is not being applied at the same place as the data. Moving either one should fix the Binding issues but one or the other may work better for your situation:
item.Header = view;
or
(item.Header as ContentControl).ContentTemplate = (DataTemplate)FindResource("ClosableTabTemplate");
Related
I made a main window that displays various user controls in a content control. In this window, I have the user controls and their accompanying view models in the XAML as DataTemplate Resources. This window has a button that needs to display the user control in the contentcontrol and instantiate the view model for it. How can i pass the resource to my RelayCommand, so that i can tell the command which user control and view model to use? I figured out how to pass a hard-coded string as the command parameter, but now I'm wanting to pass the x:Name so i can reuse this command etc for more than one View-ViewModel.
Main Window's XAML snippets:
<Window.Resources>
<!--User Controls and Accompanying View Models-->
<DataTemplate DataType="{x:Type EmployerSetupVM:EmployerSetupVM}" x:Key="EmployerSetup" x:Name="EmployerSetup">
<EmployerSetupView:EmployerSetupView />
</DataTemplate>
<DataTemplate DataType="{x:Type VendorSetupVM:VendorSetupVM}">
<VendorSetupView:VendorSetupView />
</DataTemplate>
</Window.Resources>
<Button Style="{StaticResource LinkButton}" Command="{Binding ShowCommand}" CommandParameter="{StaticResource EmployerSetup}">
...
In the Main Window's ViewModel, here relevant code so far:
public RelayCommand<DataTemplate> ShowCommand
{
get;
private set;
}
ShowCommand = new RelayCommand<string>((s) => ShowExecuted(s));
private void ShowExecuted(DataTemplate s)
{
var fred = (s.DataType); //how do i get the actual name here, i see it when i hover with intellisense, but i can't access it!
if (!PageViewModels.Contains(EmployerSetupVM))
{
EmployerSetupVM = new EmployerSetupVM();
PageViewModels.Add(EmployerSetupVM);
}
int i = PageViewModels.IndexOf(EmployerSetupVM);
ChangeViewModel(PageViewModels[i]);
}
...
in other words, how do i get the name of the my DataTemplate w/ x:Key="EmployerSetup" in the XAML? If it matters, I'm using MVVMLight too
Try using the Name property of the class Type:
private void ShowExecuted(DataTemplate s) {
var typeName = s.DataType as Type;
if (typeName == null)
return;
var className = typeName.Name; // className will be EmployerSetupVM or VendorSetupVM
...
}
I'd still say passing the DataTemplate to the VM just seems strange. I'd just have two commands and switch the command used in the Button.Style according to the conditions you got.
If you "have" to use a single RelayCommand or the world might end, I'd tend to use a static enum that you can reference from xaml for CommandParameter than pass the whole DataTemplate object.
After researching a solution to this for three days, finally I gave up. Now I need your help to solve this.
Scenario:
I've a GridView in a Usercontrol (Lets say WLMSLogs.xaml) and My GridView ItemSource is binded to a List from the ViewModel (WMLSLogsViewModel.cs)
Lets say the List has 4 items (EventID, Name, Request and Response). BothRequest and Responses are XML Strings.
GridView needs to display some of the List items in RowDetailsTemplate under different tab items. So I'm displaying Request and Response under respective TabItems.
<GridView x:Name="WMLSLogGrid" ItemsSource="{Binding WMLSLogModelList}">
<GridView.Columns>
<GridViewDataColumn DataMemberBinding="{Binding ID}" Header="ID"/>
<GridViewDataColumn DataMemberBinding="{Binding UserName}" Header="UserName"/>
</GridView.Columns>
<GridView.RowDetailsTemplate>
<DataTemplate >
<TabControl>
<TabItem Header="Request Xml">
<TextBlock text="{Binding Request}"/>
</TabItem>
<TabItem Header="Response Xml">
<TextBlock text="{Binding Response}"/>
</TabItem>
<TabItem Header="EventLogs" Tag="{Binding .}">
<views:LogEvents />
</TabItem>
</TabControl>
</Grid>
</DataTemplate>
</GridView.RowDetailsTemplate>
WMLSLogModelList is a Observable collection in WMLSLogsViewModel.cs
So far everything works fine and Grid is displaying data as expected.
Now When User expands any row, he can see two TabItems with request and response.
Here I need to add one more TabItem (LogEvents) besides Request and Response tabs.
This LogEvents tab is going have one more GridView to display (so I added a new View <views:LogEvents /> in the tab). Here comes the tricky part.
This LogEvents GridView needs to get the data based on the corresponding Selecteditem (which is EventId), and pass this EventId to a different ViewModel (LogEventViewModel.cs) and binds the data to the Inner GridView dynamically. All this has to happen either as I expand the RowDetails section or if I select the Tab LogEvents.
There is no relation between the data items of these two Grids, except getting the Selected EventId of the main GridView and passing this to a different ViewModel then to Domain service to get the inner GridView Data.
What I did so far
As I mentioned I created a new View UserControl for LogEvents, Placed it under new TabItem(EventLogs) inside row details template of main GridView.
LogEvent UserControl contains a Grid View binded to LogEventsViewModel to get the Collection based on Selected row EventId.
How do I assign the Selected EventId to a new ViewModel and Get the data dynamically?
One Way: As I showed you, I called LogEvents by placing it in side TabItem. whenever I expanded any row, Then It is Initializing the LogEvents page, During that I tried to bind the Datacontext to LogEventsViewModel. But I'm unable to get the Seleted row EventId dynamically. If I get that then I can easily pass it to the LogEventsViewModel constructor.
var _viewModel = new LogEventsViewModel(EventId);
this.DataContext = _viewModel;
InitializeComponent();
Other way:
Pass the selected EventId directly from xaml View binding to that page initialization and then to LogEventsViewModel
Something like this
<views:LogEvents something="{Binding EventId}"/>
Is there any other way to do this?
Details:
LogEvents.xaml
<UserControl.Resources>
<viewModel:LogEventsViewModel x:Key="ViewModel"/>
</UserControl.Resources>
<Grid DataContext="{Binding Source={StaticResource ViewModel}}">
<GridView x:Name="LogEventsGrid" ItemsSource="{Binding View}"
<GridView.Columns>
<telerik:GridViewToggleRowDetailsColumn />
<telerik:GridViewDataColumn DataMemberBinding="{Binding EventId}" Header="LogEventId"/>
<telerik:GridViewDataColumn DataMemberBinding="{Binding Exception}" Header="Exception" />
</GridView.Columns>
</telerik:RadGridView>
</Grid>
LogEvents.xaml.cs
public int EventId
{
get { return (int)GetValue(EventIdProperty); }
set { SetValue(EventIdProperty, value); }
}
public static readonly DependencyProperty EventIdProperty =
DependencyProperty.Register("EventId", typeof(int), typeof(ApplicationLog),
new PropertyMetadata(0, new PropertyChangedCallback(OnEventIdChanged)));
private static void OnEventIdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
int LogEventId1 = (int)e.NewValue;
// Need to assign propery in LogEventsViewModel
}
LogEventsViewModel.cs
WMLCDomainContext _Context = new WMLCDomainContext();
private QueryableDomainServiceCollectionView<LogEvents> view;
public int _eventid;
public ApplicationLogsViewModel()
{
EntityQuery<LogEvents> getLogEventsQuery = _Context.GetApplicationLogListQuery(EventId);
this.view = new QueryableDomainServiceCollectionView<ApplicationLog>(_Context, getLogEventsQuery );
}
public IEnumerable View
{get {return this.view;}}
public int EventId
{
get{return this._eventid;}
set{_eventid = value;}
}
I would create a dependency property on your LogEventsViewModel and then set up a binding on your LogEvents view, something like this:
<views:LogEvents EventId="{Binding EventId}" />
Then in LogEvents.xaml.cs you could create your dependency property:
private LogEvents_ViewModel _viewModel
{
get { return this.DataContext as LogEvents_ViewModel; }
}
public string EventId
{
get { return (string)GetValue(EventIdProperty); }
set { SetValue(EventIdProperty, value); }
}
public static readonly DependencyProperty EventIdProperty =
DependencyProperty.Register("EventId", typeof(string), typeof(LogEvents),
new PropertyMetadata(string.Empty, new PropertyChangedCallback(OnEventIdChanged)));
private static void OnEventIdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((LogEvents)d).OnTrackerInstanceChanged(e);
}
protected virtual void OnEventIdChanged(DependencyPropertyChangedEventArgs e)
{
this._viewModel.EventId = e.NewValue;
}
KodeKreachor is correct, though it may be necessary to set a Bindable attribute on your exposed property. Without it, a property might not always show in the bindable properties of the control, even if it does still work.
What would be the best way to get the elements of a combobox to each support a Command and CommandParameter?
I'd like to implement the Theme Chooser shown toward the bottom of this blog post, except with a combo box instead of a context menu. I'd need each element of the combobox to support a Command and CommandParameter, and I'd like it to just be plain text, as the combo below is.
<ComboBox>
<ComboBox.Items>
<TextBlock>A</TextBlock>
<TextBlock>B</TextBlock>
<TextBlock>C</TextBlock>
</ComboBox.Items>
</ComboBox>
I tried hyperlinks, but the main problem there is that when you click directly onto the link text, the combo box does not close.
Is there an easy way to do this?
EDIT
Ok, well the specific goal that I said I wanted to achieve—having a combo change the SL Toolkit theme—is trivially accomplished. I can simply bind the selected item of the combo to a ViewModel property that then exposes the appropriate themeuri which my SL Toolkit theme can bind to, or, since this is purely a UI activity with no business logic, I can just catch the combobox item changed event, and update my themeUri from there.
I am curious though, is there a good way to bind each combo box item to a command with a command parameter? Using a Hyperlink as each comboboxItem seemed promising, but that prevents the CB from closing after you click on an item when you click the actual hyperlink.
You could Bind the selected item to your ViewModel and then the setter would trigger when the Theme was changed.
Xaml:
<ComboBox SelectedItem="{Binding SelectedTheme, Mode=TwoWay}" ItemsSource="{Binding Themes}" />
CodeBehind:
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
DataContext = new MainPageViewModel();
}
}
ViewModel:
public class MainPageViewModel : INotifyPropertyChanged
{
public ObservableCollection<string> Themes { get; set; }
private string _selectedTheme;
public string SelectedTheme
{
get { return _selectedTheme; }
set
{
_selectedTheme = value;
// Change the Theme
RaisePropertyChanged("SelectedTheme");
}
}
public MainPageViewModel()
{
Themes = new ObservableCollection<string>();
Themes.Add("Red");
Themes.Add("Green");
Themes.Add("Blue");
}
}
I’m in the process of learning the Prism framework and I’ve come along way already. But I was wondering about how to create toolbars (and context menus) where each module can register their own buttons.
For this example I want all buttons to reside in the same ToolBar control which is located in my Shell. The ToolBars ItemsSource binds to a ToolBarItems property of type ObservableCollection<FrameworkElement> in the view model. Elements can be added to this collection using a ToolBarRegistry service. This is the ViewModel:
public class ShellViewModel
{
private IToolBarRegistry _toolBarRegistry;
private ObservableCollection<FrameworkElement> _toolBarItems;
public ShellViewModel()
{
_toolBarItems = new ObservableCollection<FrameworkElement>();
_toolBarRegistry = new ToolBarRegistry(this);
}
public ObservableCollection<FrameworkElement> ToolBarItems
{
get { return _toolBarItems; }
}
}
Note that the collection of type FrameworkElement will be refactored to be of a more concrete type if this turns out to be the correct solution.
My ToolBarRegistry has a method to register image buttons:
public void RegisterImageButton(string imageSource, ICommand command)
{
var icon = new BitmapImage(new Uri(imageSource));
var img = new Image();
img.Source = icon;
img.Width = 16;
var btn = new Button();
btn.Content = img;
btn.Command = command;
_shellViewModel.ToolBarItems.Add(btn);
}
I call this method from my OrderModule and the buttons show up correctly. So far so good.
The problem is how I can control when these buttons should be removed again. If I navigate to a view in another module (and sometimes another view in the same module), I want these module-specific buttons to be hidden again.
Do you have any suggestions on how to do this? Am I approaching this problem the wrong way, or can I modify what I already have? How did you solve this problem?
I would not insert Button instances in the ObservableCollection. Think about this approach instead:
Create ViewModel for the toolbar buttons
class ToolBarButtonViewModel : INotifyPropertyChanged
{
// INotifyPropertyChanged implementation to be provided by you
public string ImageSource { get; set; }
public ICommand Command { get; set; }
public bool IsVisible { get; set; }
}
Then of course change the type of ToolBarItems to a collection of these.
In your ShellView, add a DataTemplate for ToolBarButtonViewModel and bind the ItemsSource of whatever your toolbar control is to the collection of ViewModels, for example:
<DataTemplate>
<Button Command="{Binding Command}">
<Button.Content>
<Image Source="{Binding ImageSource}" />
</Button.Content>
</Button>
</DataTemplate>
You can now bind Button.Visibility to IsVisible with a BooleanToVisibilityConverter to solve your immediate problem.
As an added bonus, you can also:
Change the visual appearance of the toolbar buttons entirely from XAML
Bind any property of the visual tree for a toolbar button to corresponding properties on the ToolBarButtonViewModel
Update
The mechanism for enabling/disabling buttons depends on specifics of your application. There are many options -- here are a few (keep this chart in mind while reading):
Implement INavigationAware in your Views or ViewModels and enable/disable buttons as required
Attach handlers to the events of IRegionNavigationService of the region(s) of interest and have the handlers enable or disable buttons
Route all navigation through your own code (CustomNavigationService) and decide what to do inside it
I have a datagrid that is multi-select enabled. I need to change the selection in the viewmodel. However, the SelectedItems property is read only and can't be directly bound to a property in the viewmodel. So how do I signal to the view that the selection has changed?
Andy is correct. DataGridRow.IsSelected is a Dependency Property that can be databound to control selection from the ViewModel. The following sample code demonstrates this:
<Window x:Class="DataGridMultiSelectSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:tk="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
Title="Window1" Height="300" Width="300">
<StackPanel>
<tk:DataGrid AutoGenerateColumns="False" ItemsSource="{Binding}" EnableRowVirtualization="False">
<tk:DataGrid.Columns>
<tk:DataGridTextColumn Header="Value" Binding="{Binding Value}" />
</tk:DataGrid.Columns>
<tk:DataGrid.RowStyle>
<Style TargetType="tk:DataGridRow">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</tk:DataGrid.RowStyle>
</tk:DataGrid>
<Button Content="Select Even" Click="Even_Click" />
<Button Content="Select Odd" Click="Odd_Click" />
</StackPanel>
</Window>
using System.ComponentModel;
using System.Windows;
namespace DataGridMultiSelectSample
{
public partial class Window1
{
public Window1()
{
InitializeComponent();
DataContext = new[]
{
new MyViewModel {Value = "Able"},
new MyViewModel {Value = "Baker"},
new MyViewModel {Value = "Charlie"},
new MyViewModel {Value = "Dog"},
new MyViewModel {Value = "Fox"},
};
}
private void Even_Click(object sender, RoutedEventArgs e)
{
var array = (MyViewModel[]) DataContext;
for (int i = 0; i < array.Length; ++i)
array[i].IsSelected = i%2 == 0;
}
private void Odd_Click(object sender, RoutedEventArgs e)
{
var array = (MyViewModel[])DataContext;
for (int i = 0; i < array.Length; ++i)
array[i].IsSelected = i % 2 == 1;
}
}
public class MyViewModel : INotifyPropertyChanged
{
public string Value { get; set; }
private bool mIsSelected;
public bool IsSelected
{
get { return mIsSelected; }
set
{
if (mIsSelected == value) return;
mIsSelected = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("IsSelected"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
Be sure to set EnableRowVirtualisation="False" on the DataGrid element, else there's a risk that the IsSelected bindings fall out of kilter.
I haven't worked with the DataGrid much, but one technique that works for the ListView is to bind to the IsSelected property of the individual ListViewItem. Just set this to true for each object in your list, and then it will get selected.
Maybe the object that represents a row in the DataGrid also has an IsSelected property, and can be used in this way as well?
Guys, thanks for the help. My problem was solved. I think the problem is pretty common for new WPF developers, so I will restate my problem and as well as the solution in more details here just in case someone else runs into the same kind of problems.
The problem: I have a multi-select enabled datagrid of audio files. The grid has multiple column headers. The user can multi-select several row. When he clicks the Play button, the audio files will be played in the order of one the columns headers (say column A). When playback starts, the multi-select is cleared and only the currently playing file is highlighted. When playback is finished for all files, the multi-selection will be re-displayed. The playback is done in the viewmodel. As you can see, there are two problems here: 1) how to select the currently playing file from the viewmodel, and 2) how to signal to the view from the viewmodel that playback is finished and re-display the multi-selection.
The solution: To solve the first problem, I created a property in the viewmodel that is bound to the view's SelectedIndex property to select the currently playing file. To solve the second problem, I created a boolean property in the view model to indicate playback is finished. In the view's code behind, I subscribed the the boolean property's PropertyChanged event. In the event handler, the view's SelectedItems property is re-created from the saved multi-selection (the contents of SelectedItems was saved into a list and SelectedItems was cleared when playback started). At first, I had trouble re-creating SelectedItems. It turned out the problem was due to the fact that re-creation was initiated through a second thread. WPF does not allow that. The solution to this is to use the Dispatcher.Invoke() to let the main thread do the work. This may be a very simple problem for experienced developers, but for newbies, it's a small challenge. Anyway, a lot of help from different people.
Just use SelectedItems on any MultiSelector derived class , and use methods Add, Remove, Clear on IList it returns .