I'm trying to get the TreeView control in my application to properly bind to a tree of objects by setting its ItemsSource and DataContext properties. The tree visualizes as expected, but the TreeViewItems data context seems to hold incorrect values.
For example, I have a tree that looks like this:
[-] Header
[-] Contents
[+] Item1
[+] Item2
properties
[+] Dictionary
[-] MetaDictionary
[+] TypeDef1
[+] TypeDef2
properties
The items are bound to the objects' Data.Name value. However, if I click any item that is a child of Header and examine it in the event handler, its DataContext.Data.Name says Header (after appropriate castings). Same thing happens with MetaDictionary and its children.
This is a shortened version of my class:
public class CFItemTreeNode
{
private CFItem data;
public CFItem Data
{
get { return data; }
set { data = value; }
}
private ObservableCollection<CFItemTreeNode> children;
public ObservableCollection<CFItemTreeNode> Children
{
//set & get as above
}
private CFItemTreeNode parent;
public CFItemTreeNode Parent
{
//set & get as above
}
}
And this is my XAML. I've been scouring SO and the net for several days and I've incorporated bits and pieces of various tutorials and questions into this Frankenstein of mine. I believe it's a problem with the hierarchical template, but that's as far as I've gotten.
<Window x:Class="SSLowLevelBrowser.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SSLowLevelBrowser"
Title="MainWindow"
Height="600"
Width="800"
MinHeight="100"
MinWidth="200"
Closing="MainWindowClosing">
<Window.Resources>
<Style x:Key="TreeViewItemStyle" TargetType="{x:Type TreeViewItem}">
<!-- A margin of 0 px left and 2 px top -->
<Setter Property="Margin" Value="0 2" />
<Setter Property="AllowDrop" Value="true" />
<EventSetter Event="TreeViewItem.PreviewMouseLeftButtonDown" Handler="TVI_PreviewMouseLeftButtonDown" />
<EventSetter Event="TreeViewItem.PreviewMouseMove" Handler="TVI_PreviewMouseMove" />
<EventSetter Event="TreeViewItem.PreviewDrop" Handler="TVI_PreviewDrop" />
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="25" />
<RowDefinition Height="575*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="390*" />
<ColumnDefinition Width="390*" />
</Grid.ColumnDefinitions>
<ToolBar Name="menuBar" Grid.ColumnSpan="2" ToolBarTray.IsLocked="True">
<Button Name="buttonOpen" Click="OpenFile">Open file</Button>
</ToolBar>
<TreeView Grid.Row="1"
Grid.Column="0"
Name="treeView"
ItemContainerStyle="{StaticResource TreeViewItemStyle}"
ItemsSource="{Binding}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:CFItemTreeNode}" ItemsSource="{Binding Children}">
<Grid>
<TextBlock Text="{Binding Path=Data.Name}"
MouseLeftButtonDown="TBlock_PreviewMouseLeftButtonDown"/>
<TextBox Text="{Binding Path=Data.Name, Mode=TwoWay}"
Visibility="Collapsed"
LostFocus="TBox_LostFocus"/>
</Grid>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
<TextBox Grid.Row="1" Grid.Column="1" Name="textOutput" />
</Grid>
</Window>
What am I doing wrong?
Update 1. Here are my event handlers:
private void TVI_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs args)
{
dragStartPosition = args.GetPosition(null);
dragSource = args.OriginalSource;
}
private void TVI_PreviewMouseMove(object sender, MouseEventArgs args)
{
Point currentPosition = args.GetPosition(null);
// If there is actual movement and a drag is starting
if (dragInProgress == false &&
dragStartPosition.X != -1 &&
args.LeftButton == MouseButtonState.Pressed &&
Math.Pow(currentPosition.X - dragStartPosition.X, 2) +
Math.Pow(currentPosition.Y - dragStartPosition.Y, 2) > 25)
{
dragInProgress = true;
DragDropEffects de = DragDrop.DoDragDrop(
(TreeViewItem)sender,
new DataObject(typeof(FrameworkElement), dragSource),
DragDropEffects.Move);
}
}
private void TVI_PreviewDrop(object sender, DragEventArgs args)
{
if (dragInProgress && args.Data.GetDataPresent(typeof(FrameworkElement)))
{
CFItemTreeNode dragSource =
((CFItemTreeNode)((FrameworkElement)args.Data.GetData(typeof(FrameworkElement))).DataContext);
CFItemTreeNode dropTarget =
((CFItemTreeNode)((FrameworkElement)args.OriginalSource).DataContext);
CFItemTreeNode sourceParent = dragSource.Parent;
CFItemTreeNode targetParent = dropTarget.Parent;
if (sourceParent != targetParent)
{
MessageBox.Show("Can only swap siblings.");
dragInProgress = false;
return;
}
int sourceIndex = sourceParent.Children.IndexOf(dragSource);
int targetIndex = sourceParent.Children.IndexOf(dropTarget);
if (sourceIndex != targetIndex)
{
if (sourceIndex < targetIndex)
{
sourceParent.Children.RemoveAt(targetIndex);
sourceParent.Children.RemoveAt(sourceIndex);
sourceParent.Children.Insert(sourceIndex, dropTarget);
sourceParent.Children.Insert(targetIndex, dragSource);
}
else
{
sourceParent.Children.RemoveAt(sourceIndex);
sourceParent.Children.RemoveAt(targetIndex);
sourceParent.Children.Insert(targetIndex, dragSource);
sourceParent.Children.Insert(sourceIndex, dropTarget);
}
}
dragSource = null;
dragInProgress = false;
// Reset start position to invalid
dragStartPosition = new Point(-1, -1);
}
}
Add RelativeSource={RelativeSource AncestorType=local:MainWindow} to your binding.
Related
I have implemented a listview that appears as a popup list. Now I would like to add key functionalities to it, like if whenever up arrow is pressed in a text box it should select an item in my list view and if pressing of KEY_UP/DOWN is continued it should continue changing its index respectively.
This is the EditMessageTextBox and associated EditMessageTagPopup
This is the XAML code used:
<Grid x:Name="EditGrid"
Grid.Row="1"
Visibility="{Binding EditMessageControlVisibility}"
FocusManager.IsFocusScope="False"
VerticalAlignment="Center"
Grid.Column="1"
HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border x:Name="EditMessageBorder"
Grid.Row="0"
BorderThickness="1"
CornerRadius="1"
Margin="0,10,0,0"
BorderBrush="Gray">
<Grid>
<TextBlock FontSize="16"
Margin="10,0,0,3"
VerticalAlignment="Center"
HorizontalAlignment="Left"
Text="Edit message"
Foreground="{StaticResource brushWatermarkForeground}"
Visibility="{Binding ElementName=EditMessageTextBox, Path=Text.IsEmpty, Converter={StaticResource BooleanToVisibilityConverter}}" />
<TextBox Name="EditMessageTextBox"
Text="{Binding MessageToEdit, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
BorderBrush="Transparent"
BorderThickness="0"
Foreground="Black"
FontSize="16"
Margin="8,1,1,1"
VerticalContentAlignment="Center"
HorizontalContentAlignment="Left"
MinHeight="35"
ScrollViewer.VerticalScrollBarVisibility="Auto"
TextWrapping="Wrap"
AcceptsReturn="False"
KeyUp="OnEditMessage_KeyUp"
SpellCheck.IsEnabled="true" />
</Grid>
</Border>
<StackPanel Grid.Row="1"
Margin="0,10"
Orientation="Horizontal">
<Button Background="Transparent"
VerticalContentAlignment="Center"
Padding="5,2,5,3"
Foreground="Black"
BorderBrush="Gray"
BorderThickness="0.8"
Width="100"
materialDesign:ShadowAssist.ShadowDepth="Depth0"
Click="EditMessageCancelButton_Clicked">Cancel</Button>
<Button Name="EditMessageButton"
VerticalContentAlignment="Center"
Padding="5,2,5,3"
Background="#007a5a"
Foreground="White"
BorderBrush="#007a5a"
Margin="15,0,0,0"
materialDesign:ShadowAssist.ShadowDepth="Depth0"
BorderThickness="0.8"
IsEnabled="True"
Width="140"
Content="Save Changes"
Click="EditMessageSaveButton_Clicked" />
</StackPanel>
<Popup x:Name="EditMessageTagPopup"
AllowsTransparency="True"
IsOpen="{Binding IsOpenTagPopUp}"
StaysOpen="False"
Placement="Top"
PlacementTarget="{Binding ElementName=EditMessageTextBox}">
<Border materialDesign:ShadowAssist.ShadowDepth="Depth5"
CornerRadius="5"
Background="White"
BorderBrush="Black"
BorderThickness="0.8"
MaxHeight="200">
<ListView x:Name="EditTaggedUsers"
Focusable="True"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Source={StaticResource UserListForTag}}"
SelectionChanged="EditMessageTagList_SelectionChanged">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListViewItem">
<Border Name="_Border"
Padding="8">
<ContentPresenter />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver"
Value="True">
<Setter TargetName="_Border"
Property="Background"
Value="#FF3BD38E" />
<Setter Property="Foreground"
Value="White" />
</Trigger>
<Trigger Property="IsSelected"
Value="True">
<Setter TargetName="_Border"
Property="Background"
Value="#FF205B4B" />
<Setter Property="Foreground"
Value="White" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<Grid Margin="-15,0,0,0"
Width="500">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="70" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Rectangle Grid.Column="0"
RadiusY="5"
RadiusX="5"
Height="20"
Width="20">
<Rectangle.Fill>
<ImageBrush ImageSource="{Binding ProfileImage}"
Stretch="UniformToFill" />
</Rectangle.Fill>
</Rectangle>
<TextBlock Grid.Column="1"
Text="{Binding FullName}"
Margin="-10,0,0,0" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Border>
</Popup>
</Grid>
and here is code behind:
ApplicationContext.StoredEditingMessage = (String)ApplicationContext.EditMessageText;
var messageData = ((TextBox)sender).DataContext as ChatsModel;
var EditMessagePopup = FindEditMessagePopup(MessageList);
Border EditEessageBorder = EditMessagePopup.Child as Border;
ListView EditMessageTagList = EditEessageBorder.Child as ListView;
Dispatcher?.Invoke(() =>
{
if (_contactsViewModel.GroupedChatByDate
.Find(x => messageData != null && x.MessageGuid == messageData.MessageGuid)
.IsOpenTagPopUp == false) return;
var index = _contactsViewModel.UsersListForTag.IndexOf(_contactsViewModel.UsersListForTag.FirstOrDefault(x => x.Selected == true));
switch (e.Key)
{
case Key.Up:
if (EditMessageTagList.SelectedIndex > 0)
{
EditMessageTagList.SelectedIndex -= 1;
EditMessageTagList.ScrollIntoView(EditMessageTagList.Items[EditMessageTagList.SelectedIndex]);
}
else
{
EditMessageTagList.SelectedIndex = _contactsViewModel.UsersListForTag.Count - 1;
EditMessageTagList.ScrollIntoView(EditMessageTagList.Items[EditMessageTagList.SelectedIndex]);
}
break;
case Key.Down:
if (EditMessageTagList.SelectedIndex + 1 == _contactsViewModel.UsersListForTag.Count)
{
EditMessageTagList.SelectedIndex = 0;
_contactsViewModel.UsersListForTag[index].Selected = true;
EditMessageTagList.ScrollIntoView(EditMessageTagList.Items[EditMessageTagList.SelectedIndex]);
}
else
{
EditMessageTagList.SelectedIndex += 1;
_contactsViewModel.UsersListForTag[index].Selected = true;
EditMessageTagList.ScrollIntoView(EditMessageTagList.Items[EditMessageTagList.SelectedIndex]);
}
break;
}
_contactsViewModel.UsersListForTag.ForEach(x => x.Selected = false);
if (index != -1)
{
_contactsViewModel.UsersListForTag[index].Selected = true;
}
});
I have tried adding an item in scroll into view() instead of selected index yet no update
when there is a perfect selection made this function is invoked from code behind
private void EditMessageTagList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
try
{
var messageModel = ((ListView)sender).DataContext as ChatsModel;
if (((ListView)sender).SelectedItem is UserModel selectedUserForTag)
{
// _contactsViewModel.GroupedChatByDate.Find(x => messageModel != null && x.MessageGuid == messageModel.MessageGuid) .IsOpenTagPopUp = false;
string SelectedTag = (selectedUserForTag.Id == ApplicationContext.CurrentLoggedInUserGuid) ? $"{selectedUserForTag.UserName.Replace("(you) ", "")} " : $"{selectedUserForTag.UserName} ";
_contactsViewModel.GroupedChatByDate.Find
(x => messageModel != null && x.MessageGuid == messageModel.MessageGuid)
.MessageToEdit = "#" + SelectedTag;
}
// ((ListView) sender).SelectedItem = null;
}
catch (Exception exception)
{
LoggingManager.Error(exception);
}
}
Here is screen recording regarding issue
and
Here is working functionality
The problem is that after each navigation to the next item of the ListView you want to set focus to the selection TextBox which binds to the SelectedItem. Otherwise navigating the items of a ListView with the help of the arrow keys is already the default behavior of the ListView.
The simplest solution is to capture the keyboard input using UIElement.InputBinding on the selection TextBox (which enables to handle the keys in the view model) and then
Select the next/previous item
Scroll the SelectedItem into view
Move the focus to the selection TextBox
Move the caret of the selection TextBox to the end
DataItem.cs
class DataItem
{
public string FullName { get; set; }
public DataItem(string fullName) => this FullName = fullName;
}
ViewModel.cs
class ViewModel : INotifyPropertyChanged
{
public ObservableCollection<DataItem> DataItems { get; set; }
public ICommand SelectNextCommand => new AsyncRelayCommand(SelectNextItem);
public ICommand SelectPreviousCommand => new AsyncRelayCommand(SelectPreviousItem);
private bool IsSelectedItemChangeInternal { get; set; }
private DataItem selectedDataItem;
public DataItem SelectedDataItem
{
get => this.selectedDataItem;
set
{
this.selectedDataItem = value;
OnPropertyChanged();
// Do not filter the list when the selected item was set by the user
// e.g. by using arrow keys
if (!this.IsSelectedItemChangeInternal)
{
UpdateSearchFilter();
}
}
}
private string filterKey;
public string FilterKey
{
get => this.filterKey;
set
{
this.filterKey = value;
OnPropertyChanged();
// Only apply filters when the FilterKey was changed by the user
// e.g. by editing the edit TextBox that binds to this property
if (!this.IsSelectedItemChangeInternal)
{
ApplySearchFilter();
}
}
}
public ViewModel()
{
this.DataItems = new ObservableCollection<DataItems>();
for (var index = 0; index < 100; index++)
{
this.DataItems.Add(new DataItem("name " + index.ToString());
}
}
private void ApplySearchFilter()
{
ICollectionView collectionView = CollectionViewSource.GetDefaultView(this.Games);
this.IsSelectedItemChangeInternal = true;
collectionView.Filter = item =>
string.IsNullOrWhiteSpace(this.FilterKey) || (item as DetailItem).FullName.StartsWith(this.FilterKey);
// pre-select the first match
collectionView.MoveCurrentToFirst();
this.IsSelectedItemChangeInternal = false;
}
private void UpdateSearchFilter()
{
this.IsSelectedItemChangeInternal = true;
this.FilterKey = this.SelectedDataItem.FullName;
this.IsSelectedItemChangeInternal = false;
}
private void SelectNextItem()
{
ICollectionView collectionView = CollectionViewSource.GetDefaultView(this.DataItems);
collectionView.MoveCurrentToNext();
// Loop
if (collectionView.IsCurrentAfterLast)
{
collectionView.MoveCurrentToFirst();
}
}
private void SelectPreviousItem()
{
ICollectionView collectionView = CollectionViewSource.GetDefaultView(this.DataItems);
collectionView.MoveCurrentToPrevious();
// Loop
if (collectionView.IsCurrentBeforeFirst)
{
collectionView.MoveCurrentToLast();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName);
}
}
MainWindow.xaml.cs
private void AdjustFocus_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var listView = sender as ListView;
listView.ScrollIntoView(listView.SelectedItem);
Application.Current.Dispatcher.InvokeAsync(() =>
{
Keyboard.Focus(this.EditMessageTextBox);
this.EditMessageTextBox.CaretIndex = this.EditMessageTextBox.Text.Length;
});
}
private void AdjustFocus_OnOpened(object sender, EventArgs e)
{
this.EditTaggedUsers.Focus();
}
MainWindow.xaml
<Window>
<Window.DataContext>
<ViewModel />
</Window.DataContex>
<Grid>
<TextBox x:Name="EditMessageTextBox"
Text="{Binding FilterKey}">
<TextBox.InputBindings>
<KeyBinding Key="Down"
Command="{Binding SelectNextCommand}" />
<KeyBinding Key="Up"
Command="{Binding SelectPreviousCommand}" />
</TextBox.InputBindings>
</TextBox>
<Popup IsOpen="True"
Opened="AdjustFocus_OnOpened"
StaysOpen="False"
Placement="Top"
PlacementTarget="{Binding ElementName=EditMessageTextBox}">
<ListView IsSynchronizedWithCurrentItem="True"
Height="400"
SelectedItem="{Binding SelectedDataItem}"
ItemsSource="{Binding DataItems}"
SelectionChanged="AdjustFocus_OnSelectionChanged">
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type DataItem}">
<TextBox Text="{Binding FullName}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Popup>
</Grid>
</Window>
Remarks
As the navigation is done using the CollectionView of the ItemsSource the ListView.IsSynchronizedWithCurrentItem property must be set to true. Otherwise the navigation of the CollectionView won't effect the view.
I've a WrapPanel surrounded with a ScrollViewer, I want to find the visible elements on the screen when I click a button.
My code like:
<ScrollViewer>
<WrapPanel>
<Label Width="500" Height="500"/>
<Label Width="500" Height="500"/>
<Label Width="500" Height="500"/>
....
....
....
<Label Width="500" Height="500"/>
<Label Width="500" Height="500"/>
<Label Width="500" Height="500"/>
</WrapPanel>
</ScrollPanel>
How can I find the visible Label elements when ScrollViewer scrolled to some offset.
Hopefully this will get you started (you may want to change 'svViewportBounds.IntersectsWith' with 'Contains'.
public partial class MainWindow : Window
{
public string VisibleItems
{
get { return (string)GetValue(VisibleItemsProperty); }
set { SetValue(VisibleItemsProperty, value); }
}
public static readonly DependencyProperty VisibleItemsProperty =
DependencyProperty.Register("VisibleItems", typeof(string), typeof(MainWindow), new PropertyMetadata("??"));
public List<string> Items { get; private set; }
public MainWindow()
{
Items = new List<string>();
for (int i = 0; i < 25; ++i )
{
Items.Add("item_" + i);
}
DataContext = this;
InitializeComponent();
}
void OnScrollViewerScrollChanged(object sender, ScrollChangedEventArgs e)
{
ScrollViewer sv = (ScrollViewer)sender;
var visibleItems = new List<int>();
Rect svViewportBounds = new Rect(sv.HorizontalOffset, sv.VerticalOffset, sv.ViewportWidth, sv.ViewportHeight);
for(int i = 0; i < Items.Count; ++i)
{
var container = itemsHost.ItemContainerGenerator.ContainerFromIndex(i) as FrameworkElement;
if(container != null)
{
var offset = VisualTreeHelper.GetOffset(container);
var bounds = new Rect(offset.X, offset.Y, container.ActualWidth, container.ActualHeight);
if (svViewportBounds.IntersectsWith(bounds))
{
visibleItems.Add(i);
}
}
}
VisibleItems = string.Join(", ", visibleItems.ToArray());
}
}
<Window x:Class="WpfApplication59.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
WindowStartupLocation="CenterScreen"
Width="400"
Height="400">
<DockPanel>
<Border BorderThickness="1"
BorderBrush="Black"
DockPanel.Dock="Top">
<TextBlock Text="{Binding VisibleItems}"
Margin="5" />
</Border>
<ItemsControl ItemsSource="{Binding Items}" Name="itemsHost">
<ItemsControl.Template>
<ControlTemplate>
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
ScrollChanged="OnScrollViewerScrollChanged">
<WrapPanel IsItemsHost="True" />
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderThickness="1"
BorderBrush="Black"
Width="100"
Height="100">
<Label Content="{Binding}" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style TargetType="{x:Type ContentPresenter}">
<Setter Property="Margin"
Value="4" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</DockPanel>
I copied an example about dock panel from "WPF 4.5 Unleashed" ch.5. In the example the dock is set to be in the right of the grid So when in the code behind layer0.ColumnDefinitions.Add(column1CloneForLayer0); called, a column with content column1CloneForLayer0 will be created in the right side of the grid.
In my project I almost did the same thing, but when I call the function to add a column with some content, some empty space's created at the right side.. which is not as expected.So what's the right thing to do?
1.How to Add a column in the left side of a grid?
2.In the example the column was created yet why it's empty? Maybe Do i need to set the Z Index?
Update
I added something similar in the original example in the book and get wrong result. Below is the the source codes. I just added a leftToolBox in the left.
https://www.dropbox.com/sh/61hm139j77kz9k1/AACKqhG5uXFkQgnt8fWi4NvNa?dl=0
Update2
Relevant codes: In this case I just add a stackpanel with button in the left side and and click to call DocakPane(3), instead of giving the new column in the left, it creates column in the right.
public void DockPane(int paneNumber)
{
if (paneNumber == 1)
{
pane1Button.Visibility = Visibility.Collapsed;
pane1PinImage.Source = new BitmapImage(new Uri("pin.gif", UriKind.Relative));
// Add the cloned column to layer 0:
layer0.ColumnDefinitions.Add(column1CloneForLayer0);
// Add the cloned column to layer 1, but only if pane 2 is docked:
if (pane2Button.Visibility == Visibility.Collapsed) layer1.ColumnDefinitions.Add(column2CloneForLayer1);
}
else if (paneNumber == 2)
{
pane2Button.Visibility = Visibility.Collapsed;
pane2PinImage.Source = new BitmapImage(new Uri("pin.gif", UriKind.Relative));
// Add the cloned column to layer 0:
layer0.ColumnDefinitions.Add(column2CloneForLayer0);
// Add the cloned column to layer 1, but only if pane 1 is docked:
if (pane1Button.Visibility == Visibility.Collapsed) layer1.ColumnDefinitions.Add(column2CloneForLayer1);
}
else
{
leftpane1Button.Visibility = Visibility.Collapsed;
pane3PinImage.Source = new BitmapImage(new Uri("pin.gif", UriKind.Relative));
layer0.ColumnDefinitions.Add(testcol);
}
}
Based on the sample project provided, here is a re-write of the same using MVVM and most of the problem related to hard-coding would are gone. It is not pure MVVM but it is mostly MVVM to get started with.
start by defining ViewModels
a class to represent the dock pane
class DockablePaneVM : ViewModelBase
{
public DockablePaneVM(DockHostVM host)
{
Host = host;
}
private string _title;
public string Title
{
get { return _title; }
set
{
_title = value;
RaisePropertyChanged("Title");
}
}
private bool _isPinned;
public bool IsPinned
{
get { return _isPinned; }
set
{
_isPinned = value;
Host.PinModeChanged(this);
RaisePropertyChanged("IsPinned");
}
}
private object _content;
public object Content
{
get { return _content; }
set
{
_content = value;
RaisePropertyChanged("Content");
}
}
public DockHostVM Host { get; private set; }
public Dock Dock { get; set; }
}
host for the dockpanes, I have used collectionview for simplifying the location
class DockHostVM : ViewModelBase
{
public DockHostVM()
{
Panes = new ObservableCollection<DockablePaneVM>();
LeftPanes = new CollectionViewSource() { Source = Panes }.View;
RightPanes = new CollectionViewSource() { Source = Panes }.View;
FlotingLeftPanes = new CollectionViewSource() { Source = Panes }.View;
FlotingRightPanes = new CollectionViewSource() { Source = Panes }.View;
LeftPanes.Filter = o => Filter(o, Dock.Left, true);
RightPanes.Filter = o => Filter(o, Dock.Right, true);
FlotingLeftPanes.Filter = o => Filter(o, Dock.Left, false);
FlotingRightPanes.Filter = o => Filter(o, Dock.Right, false);
}
private bool Filter(object obj, Dock dock, bool isPinned)
{
DockablePaneVM vm = obj as DockablePaneVM;
return vm.Dock == dock && vm.IsPinned == isPinned;
}
public ObservableCollection<DockablePaneVM> Panes { get; set; }
public ICollectionView LeftPanes { get; set; }
public ICollectionView RightPanes { get; set; }
public ICollectionView FlotingLeftPanes { get; set; }
public ICollectionView FlotingRightPanes { get; set; }
private object _content;
public object Content
{
get { return _content; }
set { _content = value; }
}
public void PinModeChanged(DockablePaneVM paneVM)
{
LeftPanes.Refresh();
RightPanes.Refresh();
FlotingLeftPanes.Refresh();
FlotingRightPanes.Refresh();
}
//sample generator
public static DockHostVM GetSample()
{
DockHostVM vm = new DockHostVM();
vm.Panes.Add(new DockablePaneVM(vm) { Title = "Left Toolbox", Content = new ToolBoxVM() });
vm.Panes.Add(new DockablePaneVM(vm) { Title = "Solution Explorer", Content = new SolutionExplorerVM(), Dock = Dock.Right });
vm.Panes.Add(new DockablePaneVM(vm) { Title = "Toolbox", Content = new ToolBoxVM(), Dock = Dock.Right });
return vm;
}
}
then styles, to give provide the view for the classes
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:VisualStudioLikePanes">
<DataTemplate DataType="{x:Type l:DockablePaneVM}">
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>
<TextBlock Padding="8,4"
Text="{Binding Title}"
TextTrimming="CharacterEllipsis"
Background="{DynamicResource {x:Static SystemColors.ActiveCaptionBrushKey}}"
Foreground="{DynamicResource {x:Static SystemColors.ActiveCaptionTextBrushKey}}" />
<ToggleButton IsChecked="{Binding IsPinned}"
Grid.Column="1">
<Image Name="pinImage"
Source="pinHorizontal.gif" />
</ToggleButton>
<ContentControl Content="{Binding Content}"
Grid.Row="1"
Grid.ColumnSpan="2" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsPinned}"
Value="True">
<Setter Property="Source"
TargetName="pinImage"
Value="pin.gif" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
<Style TargetType="TabItem">
<Setter Property="Header"
Value="{Binding Title}" />
</Style>
<Style TargetType="TabItem"
x:Key="FloterItem"
BasedOn="{StaticResource {x:Type TabItem}}">
<Setter Property="LayoutTransform">
<Setter.Value>
<RotateTransform Angle="90" />
</Setter.Value>
</Setter>
</Style>
<Style TargetType="TabControl">
<Setter Property="l:TabControlHelper.IsLastItemSelected"
Value="True" />
<Style.Triggers>
<Trigger Property="HasItems"
Value="false">
<Setter Property="Visibility"
Value="Collapsed" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="TabControl"
x:Key="AutoResizePane"
BasedOn="{StaticResource {x:Type TabControl}}">
<Style.Triggers>
<Trigger Property="IsMouseOver"
Value="false">
<Setter Property="Width"
Value="23" />
</Trigger>
</Style.Triggers>
</Style>
<DataTemplate DataType="{x:Type l:ToolBoxVM}">
<ListBox Padding="10"
Grid.Row="1">
<ListBoxItem>Button</ListBoxItem>
<ListBoxItem>CheckBox</ListBoxItem>
<ListBoxItem>ComboBox</ListBoxItem>
<ListBoxItem>Label</ListBoxItem>
<ListBoxItem>ListBox</ListBoxItem>
</ListBox>
</DataTemplate>
<DataTemplate DataType="{x:Type l:SolutionExplorerVM}">
<TreeView Grid.Row="2">
<TreeViewItem Header="My Solution" IsExpanded="True">
<TreeViewItem Header="Project #1" />
<TreeViewItem Header="Project #2" />
<TreeViewItem Header="Project #3" />
</TreeViewItem>
</TreeView>
</DataTemplate>
</ResourceDictionary>
I have also created some dummy classes to represent the toolbox and solution explorer
also a helper class to improve the usability of tab control which i have used to host the dockpanes
class TabControlHelper
{
public static bool GetIsLastItemSelected(DependencyObject obj)
{
return (bool)obj.GetValue(IsLastItemSelectedProperty);
}
public static void SetIsLastItemSelected(DependencyObject obj, bool value)
{
obj.SetValue(IsLastItemSelectedProperty, value);
}
// Using a DependencyProperty as the backing store for IsLastItemSelected. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsLastItemSelectedProperty =
DependencyProperty.RegisterAttached("IsLastItemSelected", typeof(bool), typeof(TabControlHelper), new PropertyMetadata(false, OnIsLastItemSelected));
private static void OnIsLastItemSelected(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TabControl tc = d as TabControl;
tc.Items.CurrentChanged += (ss, ee) =>
{
if (tc.SelectedIndex < 0 && tc.Items.Count > 0)
tc.SelectedIndex = 0;
};
}
}
this will keep an item selected any time, in this project it will be used when a dock pane is pinned/unpinned
now the main window, note that I have bounded the dock Panes to 4 tab controls, left, right, left float & right float
<Window x:Class="VisualStudioLikePanes.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
Height="500"
Width="800"
WindowStartupLocation="CenterScreen"
xmlns:l="clr-namespace:VisualStudioLikePanes">
<Window.DataContext>
<ObjectDataProvider MethodName="GetSample"
ObjectType="{x:Type l:DockHostVM}" />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Menu>
<MenuItem Header="FILE" />
...
<MenuItem Header="HELP" />
</Menu>
<DockPanel LastChildFill="True"
Grid.Row="1">
<Border Width="23"
DockPanel.Dock="Left"
Visibility="{Binding Visibility, ElementName=LeftFloter}" />
<Border Width="23"
DockPanel.Dock="Right"
Visibility="{Binding Visibility, ElementName=RightFloter}" />
<TabControl ItemsSource="{Binding LeftPanes}"
DockPanel.Dock="Left" />
<TabControl ItemsSource="{Binding RightPanes}"
DockPanel.Dock="Right" />
<Grid Name="layer0">
... page content
</Grid>
</DockPanel>
<TabControl ItemsSource="{Binding FlotingLeftPanes}"
Grid.Row="1"
HorizontalAlignment="Left"
TabStripPlacement="Left"
Style="{StaticResource AutoResizePane}"
ItemContainerStyle="{StaticResource FloterItem}"
x:Name="LeftFloter" />
<TabControl ItemsSource="{Binding FlotingRightPanes}"
Grid.Row="1"
HorizontalAlignment="Right"
TabStripPlacement="Right"
Style="{StaticResource AutoResizePane}"
ItemContainerStyle="{StaticResource FloterItem}"
x:Name="RightFloter" />
</Grid>
</Window>
result is your expected behavior with MVVM approach, adding new panel is easy as Panes.Add(new DockablePaneVM(vm) { Title = "Left Toolbox", Content = new ToolBoxVM() }); rest is handled.
Demo
download the working sample VisualStudioLikePanes.zip
I want to implement a custom add row button to a DataGrid (it's a long story). I added the button in template, and define an attached property, and I can get the button's click. But I can not add a new row in a generic way -not for a specified type-. I know that I can do similar thing in ViewModel, but I'm looking to do this in templates and attached properties. Here is my try; Any idea to complete this?
XAML:
<Style x:Key="{x:Type DataGrid}" TargetType="{x:Type DataGrid}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type DataGrid}">
<Border x:Name="border">
<ScrollViewer x:Name="DG_ScrollViewer" Focusable="false">
<ScrollViewer.Template>
<ControlTemplate TargetType="{x:Type ScrollViewer}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button Focusable="false"
Command="{x:Static DataGrid.SelectAllCommand}" />
<DataGridColumnHeadersPresenter
x:Name="PART_ColumnHeadersPresenter"
Grid.Column="1" />
<Grid Grid.ColumnSpan="2" Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="22" />
</Grid.RowDefinitions>
<ScrollContentPresenter
x:Name="PART_ScrollContentPresenter" />
<!-- THIS IS MY CUSTOM BUTTON TO ADD NEW ROW -->
<Button x:Name="PART_AddRowButton"
Content="Add"/>
</Grid>
<ScrollBar x:Name="PART_VerticalScrollBar"/>
<Grid Grid.Column="1" Grid.Row="2">
<ScrollBar x:Name="PART_HorizontalScrollBar"/>
</Grid>
</Grid>
</ControlTemplate>
</ScrollViewer.Template>
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
C#:
public class DataGridHelper {
public static readonly DependencyProperty CanUserAddRowsProperty
= DependencyProperty.RegisterAttached(
"CanUserAddRows", typeof(bool), typeof(DataGridHelper),
new FrameworkPropertyMetadata(default(bool), CanUserAddRowsChanged));
[AttachedPropertyBrowsableForType(typeof(DataGrid))]
public static bool GetCanUserAddRows(DependencyObject obj) {
return (bool)obj.GetValue(CanUserAddRowsProperty);
}
[AttachedPropertyBrowsableForType(typeof(DataGrid))]
public static void SetCanUserAddRows(DependencyObject obj, bool value) {
obj.SetValue(CanUserAddRowsProperty, value);
}
private static void CanUserAddRowsChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e) {
var dataGrid = d as DataGrid;
if (dataGrid == null)
return;
bool oldValue = (bool)e.OldValue,
newValue = (bool)e.NewValue;
if (newValue == oldValue)
return;
if (newValue) {
dataGrid.Loaded += CanUserAddRowsDataGridLoaded;
} else {
dataGrid.Loaded -= CanUserAddRowsDataGridLoaded;
}
}
private static void CanUserAddRowsDataGridLoaded(object sender, RoutedEventArgs e) {
var dataGrid = sender as DataGrid;
if (dataGrid == null)
return;
if (dataGrid.Style == null)
return;
var rootTemplate = dataGrid.Template;
if (rootTemplate == null)
return;
var scroll = rootTemplate.FindName("DG_ScrollViewer", dataGrid) as ScrollViewer;
if (scroll == null)
return;
var scrollTemplate = scroll.Template;
if (scrollTemplate == null)
return;
var button = scrollTemplate.FindName("PART_AddRowButton", scroll) as ButtonBase;
if (button == null)
return;
if (GetCanUserAddRows(dataGrid)) {
button.Click += AddRowClicked;
} else {
button.Click -= AddRowClicked;
}
}
private static void AddRowClicked(object sender, RoutedEventArgs e) {
var button = ((ButtonBase)sender);
var parent = VisualTreeHelper.GetParent(button);
while (!(parent is DataGrid))
parent = VisualTreeHelper.GetParent(parent);
var source = ((DataGrid)parent).Items.Add(...) // now what???
}
}
Well, as you can see, I got access to DataGrid after button got clicked; But what is next? How can I force the DataGrid to show NewItemPlaceHolder?
Typically in WPF, we bind collections (preferably collections that support change notification like ObservableCollection) of data objects to UI controls. Rather than adding new items to the UI controls, we add the items to the collections in the code behind/view model. As long as the collection support change notification, the UI controls will be automatically updated.
So to add a new row to your DataGrid, you need to add a new item to your collection:
dataCollection.Add(new DataType());
You should be able to access your data bound collection in your AttachedProperty using:
var dataCollection = (DataCollectionType)dataGrid.ItemsSource;
I believe that you can also use:
dataGrid.Items.Add(new DataType());
although this method is not recommended.
I have a ListboxItem with a checkbox in its template. When I click the checkbox, a section of the template gets visible. That works ok.
I am trying to simulate the same behaviour by clicking the item itself making it expand/collapse the respective section. It should always negate the current state of the item(expanded/collapsed)
I am using C#/WPF
<Grid x:Name="gridExpanded"
HorizontalAlignment="Stretch"
Margin="8"
Grid.RowSpan="1"
Width="Auto"
Height="Auto"
Visibility="{Binding IsChecked, Converter={StaticResource booleanToVisibilityConverter}, ElementName=checkBox}" />
It sounds like you are actually looking for the Expander control. This allows you to specify a header and content, and clicking on the header will toggle the visibility of the content
By WPF ListBox does not change CheckBox state when the corresponding label is clicked.
To solve this,
1) Add a IsVisibleFlag property to the item model
2) Add a handler for the PreviewMouseLeftButtonDown event of the item
3) In the handler use ItemContainerGenerator.ContainerFromItem to update the visibility flag on click
4) Associate the visibility of your template section with the IsVisibleFlag (or with the checkBox state).
The ItemModel:
publibc class MyItemModel : INotifyPropertyChanged
{
private bool _isVisibleFlag;
public bool IsVisibleFlag
{
get { return _isVisibleFlag; }
set
{
if (_isVisibleFlag != value)
{
_isVisibleFlag = value;
OnPropertyChanged(() => IsVisibleFlag);
}
}
}
// ItemText property goes here (I ommited it to save space)
}
In XAML:
<Window
<!--generated x:Class and xmlns goes here (I ommited them to save space) -->
DataContext="{Binding RelativeSource={RelativeSource Self}}"
>
<Window.Resources>
<Style TargetType="ListBoxItem">
<EventSetter Event="PreviewMouseLeftButtonDown" Handler="ListViewItem_PreviewMouseLeftButtonDown" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<CheckBox Name="chkVisible" Grid.Column="0" IsChecked="{Binding IsVisibleFlag}" />
<TextBlock Grid.Column="1" Text="{Binding ItemText}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<ListBox Name="MyListBox" ItemsSource="{Binding AddableWidgets}" />
</Grid>
</Window>
In code:
private void ListViewItem_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
for (int i = 0; i < MyListBox.Items.Count; i++)
{
object yourObject = MyListBox.Items[i];
ListBoxItem lbi = (ListBoxItem)MyListBox.ItemContainerGenerator.ContainerFromItem(yourObject);
if (lbi.IsFocused)
{
MyItemModel w = (MyItemModel)MyListBox.Items[i];
w.IsVisibleFlag = !w.IsVisibleFlag;
e.Handled = true;
}
}
}