WPF Custom Control for Expander - c#

I am working with WPF and looking for a best approach for creating re-usable expanders that are customized. Specifically the Expander header would remain the same, the content would vary.
The Expander header would have 6 buttons all wired to the same methods, which will be enforced via an interface.
Assume this is Expander 1
And this is another expander
The actual content will be text, buttons, whatever. It just has text for demo purposes.
The expander is meant to be used within a User Control, in which I have 44 of them, and don't want to repeat the code all over the place.
At the moment I am using the UserControls like the following in the Window XAML
xmlns:customcontrols="clr-namespace:MyNamespace.Controls;assembly=MyAssembly"
And the actual usage:
<customcontrols:FlexExtend ..... />
And inside each User Control I am using expander like this
<Expander Style="{StaticResource ModToolPanelStyle}" Background="#403F3B" Name="toolExpand" Header="{x:Static properties:Resources.AdductionAbduction_Label}" Collapsed="toolExpand_Collapsed" Expanded="toolExpand_Expanded">
....... all the inside body stuff
</expander>
Right now I'm looking at having to replicate the code 44 times, one for each expander in each of the 44 user controls that contain the an expander. Is there a way in WPF to make this a custom control that would have the buttons and everything? I'm think no since it wouldn't be able to be bound there for the on click?
UPDATE:
As suggested I created a DataTemplate in a seperate XAML.
<DataTemplate x:Key="DesignExpanderHeaderTemplate">
<DockPanel>
<TextBlock Name="ModName"
Foreground="White"
Text="Balls">
</TextBlock>
<Button Name="MoveUpButton"
Content="MoveUp"
Width="80"
Height="25">
</Button>
</DockPanel>
</DataTemplate>
However now I am having issues binding the button from the user control:
var button = toolExpand.HeaderTemplate.FindName("MoveUpButton", toolExpand) as Button;
button.Click += delegate (object sender, RoutedEventArgs args)
{
MessageBox.Show("The button has been pressed");
};
The button is always null, so it is not able to find it.
This is how the XAML looks
<Expander Style="{StaticResource ModToolPanelStyle}"
Background="#403F3B"
x:Name="toolExpand"
HeaderTemplate="{StaticResource DesignExpanderHeaderTemplate}"
Collapsed="toolExpand_Collapsed"
Expanded="toolExpand_Expanded">

Following the guidance of user2455627 I was able to get this working. The main key was the following:
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}
My datatemplate looks like the following.
<DataTemplate x:Key="DesignExpanderHeaderTemplate">
<DockPanel>
<TextBlock Foreground="White"
Text="{Binding ModName, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}}">
</TextBlock>
<Button Command="{Binding MoveUpCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}}"
Content="MoveUp"
Width="80"
Height="25">
</Button>
<Button Command="{Binding MoveDownCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}}"
Content="MoveUp"
Width="80"
Height="25">
</Button>
<Button Command="{Binding UndoCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}}"
Content="Undo"
Width="80"
Height="25"></Button>
</DockPanel>
</DataTemplate>
Here is how I am using the data template in the XAML
<Expander Style="{StaticResource ModToolPanelStyle}"
Background="#403F3B"
x:Name="toolExpand"
HeaderTemplate="{StaticResource DesignExpanderHeaderTemplate}"
Collapsed="toolExpand_Collapsed"
Expanded="toolExpand_Expanded">
And here is the relevant code behind:
// Commands
public RelayCommand MoveUpCommand { get; set; }
public RelayCommand MoveDownCommand { get; set; }
public RelayCommand UndoCommand { get; set; }
public RelayCommand RedoCommand { get; set; }
public RelayCommand ClearCommnand { get; set; }
public RelayCommand RemoveCommand { get; set; }
// Properties
private string _modName;
// setup ModPanel
ModName = "Something";
MoveUpCommand = new RelayCommand(MoveUp);
MoveDownCommand = new RelayCommand(MoveDown);
UndoCommand = new RelayCommand(Undo);
RedoCommand = new RelayCommand(Redo);
ClearCommnand = new RelayCommand(Clear);
RemoveCommand = new RelayCommand(Remove);
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public string ModName
{
get { return _modName; }
set
{
_modName = value;
OnPropertyChanged();
}
}
public void MoveUp(object obj)
{
throw new NotImplementedException();
}
public void MoveDown(object obj)
{
throw new NotImplementedException();
}
public void Undo(object obj)
{
throw new NotImplementedException();
}
public void Redo(object obj)
{
throw new NotImplementedException();
}
public void Remove(object obj)
{
throw new NotImplementedException();
}
public void Clear(object obj)
{
throw new NotImplementedException();
}
public void PreApply()
{
throw new NotImplementedException();
}

Related

MouseBinding in TexBox within ItemsControl does not work WPF C#

I have my MainWindow which has an ItemsControl for my EngineersUserControl (Engineers_UC) along with other controls. My Engineers_UC consists of a few TextBoxes for which I want to add MouseBinding with the aim of being able to left click on a TextBox and another method in my ViewModel to be executed. I have read that the issue might be that the elements of ItemsControl are not focusable but I haven't found a solution. Any ideas ?
MainWindow:
<Grid>
<StackPanel>
<UserControl:Ribbon_UC Loaded="Ribbon_UC_Loaded" Margin="0,0,0,70"/>
<UserControl:Calendar_UC/>
<ItemsControl ItemsSource="{Binding Engineer}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<UserControl:Engineers_UC />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
Engineers_UC:
<TextBox Name="EngineerName" IsReadOnly="True" Style="{StaticResource StyleTrigger} Text ="{Binding FULLNAME}">
<TextBox.InputBindings>
<MouseBinding Command="{Binding EngineerCommand}" CommandParameter="{Binding ElementName=EngineerName}" MouseAction="{Binding EngineerCommand.MouseGesture}"/>
</TextBox.InputBindings>
</TextBox>
EngineerCommand:
void RelayCommands()
{
EngineerCommand = new SimpleDelegateCommand(x => EngineerFunction(x))
{
MouseGesture = MouseAction.LeftClick
};
}
void EngineerFunction (object _engineername)
{
EngineerNameClicked = (_engineername as TextBox).Text;
}
public class SimpleDelegateCommand : ICommand
{
public Key GestureKey { get; set; }
public ModifierKeys GestureModifier { get; set; }
public MouseAction MouseGesture { get; set; }
Action<object> _executeDelegate;
public SimpleDelegateCommand(Action<object> executeDelegate)
{
_executeDelegate = executeDelegate;
}
public void Execute(object parameter)
{
_executeDelegate(parameter);
}
public bool CanExecute(object parameter) { return true; }
public event EventHandler CanExecuteChanged;
}
If the EngineerCommand command is defined in the same view model class as the collection to which you bind the ItemsSource property of the ItemsControl to (Engineer), you should use a RelativeSource for your binding(s) in the ItemTemplate:
<MouseBinding Command="{Binding DataContext.EngineerCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}" ... />

Is it possible to register View with it's name via regionManager.RequestNavigate to remove it later from region?

In my PRISM-App the user can open a view of a module in a TabView (Navigate("TestView")). Now I want to close this view via OnCloseTab("TestView") but the registered view has no Name.
public class MainWindowViewModel: BindableBase
{
...
private void Navigate(string uri)
{
this.regionManager.RequestNavigate("TabRegion", uri);
}
private void OnCloseTab(string uri)
{
IRegion region = this.regionManager.Regions["TabRegion"];
object view = region.GetView(uri);
if (view != null)
{
region.Remove(view);
}
}
}
The Module is registered in my bootstrapper like this:
protected override void ConfigureModuleCatalog()
{
base.ConfigureModuleCatalog();
ModuleCatalog moduleCatalog = (ModuleCatalog)this.ModuleCatalog;
Type modulePType = typeof(Module.ProductionData.ProductionDataModule);
moduleCatalog.AddModule(typeof(Module.ProductionData.ProductionDataModule));
}
It works with:
IRegion region = regionManager.Regions["TabRegion"];
object view = region.GetView("TestView");
if (view == null)
{
view = ServiceLocator.Current.GetInstance<Views.TestView>();
region.Add(view, "TestView");
}
But the MainWindowViewModel does not know the about the views of the modules. Is there any way to remove the view, when it has no Name? Thanks for any advise
The RequestNavigate("TabRegion", uri) method internally adds the selected view to the region using the following method:
IRegionManager Add(object view, string viewName, bool createRegionManagerScope)
called with the following parameters:
RegionManager.Add(view, null, false);
so no name is associated to the navigated view. Therefore, it is not possible to retrieve the view object using the view name/uri. An alternative approach is to try to match the .net type of the view:
object view = region.Views.FirstOrDefault(v => v.GetType() == typeof(yourViewType));
If this is not enough you can still add extra properties on your view object and try to retrieve them casting the views into a suitable type.
Thank you Luke. I found a solution for my problem here
In my MainWindowView.cs (XAML) I've added the following:
<TabControl.ItemTemplate>
<DataTemplate>
<DockPanel Width="Auto">
<Button Command="{Binding DataContext.CloseTabCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType={x:Type TabItem}}}"
Content="X"
Cursor="Hand"
DockPanel.Dock="Right"
Focusable="False"
FontFamily="Courier"
FontWeight="Bold"
Margin="4,0,0,0"
FontSize="10"
VerticalContentAlignment="Center"
Width="15" Height="15" />
<ContentPresenter Content="{Binding DataContext.DataContext.ViewTitle, RelativeSource={RelativeSource AncestorType={x:Type TabItem}}}" />
</DockPanel>
</DataTemplate>
</TabControl.ItemTemplate>
In my MainWindowViewModel.cs I changed my CloseCommand like this:
public DelegateCommand<object> CloseTabCommand { get; set; }
public MainWindowViewModel(IRegionManager regionManager)
{
this.regionManager = regionManager;
CloseTabCommand = new DelegateCommand<object>(OnCloseTab);
}
private void OnCloseTab(object tabItem)
{
var view = ((System.Windows.Controls.TabItem)tabItem).DataContext;
this.regionManager.Regions["TabRegion"].Remove(view);
}

Databinding with TreeView in WPF

I am working with data binding and tree views and I am not able to get my TreeView to populate in my WPF. I think I am relatively close, just a small tweak somewhere, but I can't seem to find it.
Here's my Project class:
public class Project
{
public Project(string Name, bool isFolder, Project ParentFolder)
{
this.Name = Name;
this.isFolder = isFolder;
Children = new List<Project>();
if (ParentFolder == null)
{
Path = Name;
}
else
{
Path = ParentFolder.Path + " > " + Name;
}
}
public string Path { get; private set; }
public string Name { get; set; }
public bool isFolder { get; set; }
public List<Project> Children { get; set; }
public IEnumerable<Project> ChildFolders
{
get
{
return Children.Where(p => p.isFolder);
}
}
public object Icon
{
get
{
if (isFolder)
{
return 0; // return folder icon
}
else
{
return 1; // return project icon
}
}
}
public IEnumerable<Project> SearchRecursively(string SearchString)
{
return GetAllChildren.Where(p => p.Name.Contains(SearchString));
}
private List<Project> GetAllChildren
{
get
{
List<Project> allChildren = new List<Project>();
foreach(Project child in Children)
{
allChildren.AddRange(child.GetAllChildren);
}
return allChildren;
}
}
}
}
Here is my MaiWindow.xaml.cs class that I will be using to make test data:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.BuildData();
}
private void BuildData()
{
List<Project> parents = new List<Project>();
Project parentOne = new Project("Apple", true, null);
Project parentTwo = new Project("Samsung", true, null);
Project parentThree = new Project("Google", true, null);
parents.Add(parentOne); parents.Add(parentTwo); parents.Add(parentThree);
Project appleMacBook = new Project("Mac", false, parentOne);
Project appleIpad = new Project("iPad", false, parentOne);
Project appleiPhone = new Project("iPhone", false, parentOne);
Project samsungGalaxy = new Project("Galaxy", false, parentTwo);
Project samsungNote = new Project("Note", false, parentTwo);
Project googlePixel = new Project("Pixel", false, parentThree);
Project googleChromecast = new Project("Chromecast", false, parentThree);
parents[0].Children.Add(appleMacBook); parents[0].Children.Add(appleIpad); parents[0].Children.Add(appleiPhone);
parents[1].Children.Add(samsungGalaxy); parents[1].Children.Add(samsungNote);
parents[2].Children.Add(googlePixel); parents[2].Children.Add(googleChromecast);
}
}
}
And here is my XAML where I am trying to display the TreeView. Right now, it is just blank. I would appreciate any tips.
<TreeView x:Name="Hierarchy" Grid.Column="4" HorizontalAlignment="Left" Height="631" Margin="0,58,0,0" Grid.Row="1" VerticalAlignment="Top" Width="265"
ItemsSource="{Binding parents}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding parents}" DataType="{x:Type self:Project}">
<TreeViewItem Header="{Binding Name}"></TreeViewItem>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
Edit:
Here's the Property class:
public string Name
{
get
{
return name;
}
set
{
name = value;
OnPropertyChanged("Name");
}
}
private string name { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
XAML:
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding ChildFolders}">
<StackPanel Orientation="Horizontal" >
<Image Source="{Binding Icon}" Margin="5, 5, 5, 5"></Image>
<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" BorderThickness="0" FontSize="16" Margin="5"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
So, this doesn't seem to be firing the change event. I know this because Path is set as Name + ">". When I change the Name, Path is not reflecting the change. It only shows what my previous value for Name was, if that makes sense.
if (ParentFolder == null)
{
Path = Name;
}
else
{
Path = ParentFolder.Path + " > " + Name;
}
Edit:
public Project(string Name, bool isFolder, Project ParentFolder)
{
this.Name = Name;
this.isFolder = isFolder;
Children = new List<Project>();
this.ParentFolder = ParentFolder;
}
public string Path
{
get
{
return this.ParentFolder + " > " + this.Name;
}
set
{
this.Path = Path;
}
}
XAML:
<TextBox x:Name="FolderNameBox" Grid.Column="1" Background="White" Grid.Row="1" Grid.ColumnSpan="5"
Margin="0,0,287,654.333" VerticalContentAlignment="Center"
Padding="6" FontSize="16"
IsReadOnly="True"
Text="{Binding ElementName=Hierarchy, Path=SelectedItem.Path, UpdateSourceTrigger=PropertyChanged}">
</TextBox>
<TextBox x:Name="SearchProjectsBox" Grid.Column="5" Background="White" Grid.Row="1" Text="Search Projects"
Margin="47.333,0,0,654.333" VerticalContentAlignment="Center" Foreground="LightGray" Padding="6" FontSize="16" HorizontalAlignment="Left" Width="268" GotFocus="TextBox_GotFocus" LostFocus="TextBox_LostFocus"/>
<TreeView x:Name="Hierarchy" Grid.Column="4" HorizontalAlignment="Left" Height="631" Margin="0,58,0,0" Grid.Row="1" VerticalAlignment="Top" Width="226"
ItemsSource="{Binding Projects}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding ChildFolders}">
<StackPanel Orientation="Horizontal" >
<Image Source="{Binding Icon}" Margin="5, 5, 5, 5"></Image>
<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" BorderThickness="0" FontSize="16" Margin="5"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<Grid Grid.ColumnSpan="2" Grid.Column="4" HorizontalAlignment="Left" Height="631" Margin="245,58,0,0" Grid.Row="1" VerticalAlignment="Top" Width="540">
<ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden">
<ListView Margin="0,0,10,0" Name="ProjectView" ItemsSource="{Binding Projects}" FontSize="16" Foreground="Black">
<ListView.View>
<GridView ColumnHeaderContainerStyle="{StaticResource GridHeader}">
<GridViewColumn Header="Name" Width="200" DisplayMemberBinding="{Binding ElementName=Hierarchy, Path=SelectedItem.Name, UpdateSourceTrigger=PropertyChanged}"></GridViewColumn>
<GridViewColumn Header="Directory" Width="328" DisplayMemberBinding="{Binding ElementName=Hierarchy, Path=SelectedItem.Path, UpdateSourceTrigger=PropertyChanged}"></GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</ScrollViewer>
</Grid>
</Grid>
The Path updates too but when it I see it it will display the path of the project rather than the fired change of name. It changes in real-time but doesn't save the String value..only registers that a change has happened.
Heres my Property Change too.
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
You have a few problems here.
ItemsSource="{Binding parents}"
Here's parents:
private void BuildData()
{
List<Project> parents = new List<Project>();
You're asking XAML to examine all the methods in the codebehind class, looking for local variables named parents. This isn't a reasonable request.
There are a few requirements if you want to bind to parents: It must be...
A public...
Property (not a field -- it needs a get block)...
Of whatever object is your TreeView's DataContext.
None of those are true.
Two more things -- not required, but a very good idea:
Make it ObservableCollection<T> rather than List<T>, so that it will notify the UI of added or removed items.
The class that owns it should be a viewmodel class, not your window/usercontrol. When we say "viewmodel", we mean it implements INotifyPropertyChanged and raises PropertyChanged when its property values change. Again, this is about keeping the UI informed of changes.
Keeping the UI informed is what bindings are all about: They listen for changes in the viewmodel and update the UI.
So you need a main viewmodel that looks like this:
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
// C#6
/*
protected virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
*/
protected virtual void OnPropertyChanged(string propName)
{
var handler = PropertyChanged;
if (handler != null) {
handler(this, new PropertyChangedEventArgs(propName));
}
}
}
public class MainViewModel : ViewModelBase
{
private ObservableCollection<Project> _projects;
public ObservableCollection<Project> Projects {
get { return _projects; }
set {
if (value != _projects) {
_projects = value;
OnPropertyChanged(nameof(Projects));
}
}
}
public void BuildData() {
Projects = new ObservableCollection<Project>();
// do the rest of the stuff
}
}
And you should rewrite your Project class as a ProjectViewModel derived from ViewModelBase, make it raise PropertyChanged in the same way, and use ObservableCollection<Project> for Children.
And in your main window...
public MainWindow()
{
InitializeComponent();
var vm = new MainViewModel();
vm.BuildData();
DataContext = vm;
}
Your XAML needs a little work, too.
Projects has a capitalized name now
For the item template, you are binding to the property of the child item which provides the tree view item's children. That's the Children property of your Project class.
A datatemplate tells XAML how to present the content of a control. The tree creates a TreeViewItem with a Project as its DataContext, and then uses your HierarchicalDataTemplate to turn that DataContext into some kind of visual content. You don't use the template to create a TreeViewItem; you use it to create the visual stuff in the TreeViewItem.
So here's the new XAML:
<TreeView
x:Name="Hierarchy"
ItemsSource="{Binding Projects}"
Grid.Column="4"
HorizontalAlignment="Left"
Height="631"
Margin="0,58,0,0"
Grid.Row="1"
VerticalAlignment="Top"
Width="265"
>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Label Content="{Binding Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
There's no reason to get in the habit of DataContext = this;. Once you start that, the next thing you know you'll be doing it in a UserControl and coming here asking why all your bindings to it in the parent XAML are broken. Dependency properties are a bigger hassle than INPC, and you end up with code that ought to be in a viewmodel mixed into your MainWindow code. If you use viewmodelsit's the easiest thing in the world to shuffle your UI around. Maybe you want the original content of your main window to be just one of three tab pages in the main window. Keeping code separated properly makes that kind of thing much simpler.

WPF Tabcontrol (TabItem Content is not appearing)

I implemented a TabControl with Closable TabItems in my App. For this, I am using a Collection which I fill with the SubMenuItems of MenuItem "Öffne", which are bound to ICommands in the MainViewModel.
So if I click on MenuItem "Open Tab1", then the header of Tab 1 is created, but I can not see any content. The content of the TabItem is shown AFTER I click on the Header of the TabItem. But I want it to be shown directly when the TabItem is "created" without any need of clicking on the header. Closing the TabItems from the "X" button works fine.
I looked at a couple of examples and tried a ContentTemplate, but it didn't work (Maybe I made something wrong?).
I Hope you can tell me what i have done wrong or show me a good example.
Thanks in advance!
Here are my code snippets:
MainWindow.XAML:
<Window.Resources>
<vm:MainViewModel x:Key="viewModel"/>
</Window.Resources>
<TabControl Background="#FFE5E5E5" ItemsSource="{Binding TabControlViews}" SelectedItem="{Binding CurrentTabItem}" Margin="0,21,0,0">
<TabControl.ItemTemplate>
<DataTemplate>
<DockPanel Width="120">
<TextBlock Text="{Binding Header}"/>
<Button
Command="{Binding ParameterizedCommand, Source={StaticResource viewModel}}"
CommandParameter="{Binding Header, RelativeSource={RelativeSource AncestorType={x:Type TabItem}}}"
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" />
<ContentPresenter
Content="{Binding Path=DisplayName}"
VerticalAlignment="Center" />
</DockPanel>
</DataTemplate>
</TabControl.ItemTemplate>
<!--<TabControl.ContentTemplate>
<DataTemplate>
</DataTemplate>
</TabControl.ContentTemplate>-->
<TabControl.Resources>
<DataTemplate x:Name="test" DataType="{x:Type vm:MenueVM}">
<cu:MenueSearch/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:FieldPointsVM}">
<cu:FieldPointsSearch/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:DataTransferVM}">
<cu:DataTransfer/>
</DataTemplate>
</TabControl.Resources>
</TabControl>
MainWindow.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var vm = (MainViewModel)Resources["viewModel"];
this.DataContext = vm;
}
}
MainViewModel.cs:
public MainViewModel()
{
TabControlViews = new ObservableCollection<BaseViewModel>();
_menueVM = new MenueVM("Menüpunkte", "Menue");
_fieldVM = new FieldPointsVM("Feldpunkte", "FieldPoint");
_dataVM = new DataTransferVM("DatenTransfer", "DataTransfer");
ParameterizedCommand = new RelayCommand(DoParameterizedCommand);
}
private void DoParameterizedCommand(object parameter)
{
if (parameter.ToString() == "App.ViewModel.MenueVM")
{
TabControlViews.Remove(_menueVM);
}
else if (parameter.ToString() == "App.ViewModel.FieldPointsVM")
{
TabControlViews.Remove(_fieldVM);
}
else if (parameter.ToString() == "App.ViewModel.DataTransfer")
{
TabControlViews.Remove(_dataVM);
}
}
private ICommand _parameterizedCommand;
public ICommand ParameterizedCommand
{
get
{
return _parameterizedCommand;
}
set
{
_parameterizedCommand = value;
}
}
private TabItem _propCurrentTabItem;
public TabItem CurrentTabItem
{
get
{
return _propCurrentTabItem;
}
set
{
_propCurrentTabItem = value;
}
}
private ObservableCollection<BaseViewModel> _TabControlViews = new ObservableCollection<BaseViewModel>();
public ObservableCollection<BaseViewModel> TabControlViews
{
get
{
return _TabControlViews;
}
set
{
_TabControlViews = value;
OnPropertyChanged();
}
}
public ICommand OpenMenupunkteCommand
{
get
{
return new BaseCommand(OpenMenuPunkte);
}
}
public ICommand OpenFeldpunkteCommand
{
get
{
return new BaseCommand(OpenFeldpunkte);
}
}
public ICommand OpenDataTransferCommand
{
get
{
return new BaseCommand(OpenDataTransfer);
}
}
private void OpenMenuPunkte()
{
if (!TabControlViews.Contains(_menueVM))
{
TabControlViews.Add(_menueVM);
}
}
private void OpenFeldpunkte()
{
if (!TabControlViews.Contains(_fieldVM))
{
TabControlViews.Add(_fieldVM);
}
}
private void OpenDataTransfer()
{
if (!TabControlViews.Contains(_dataVM))
{
TabControlViews.Add(_dataVM);
}
}
MenueVM.cs
public class MenueVM : BaseViewModel
{
public MenueVM()
{
//Here are some actions done for Data, but I think they are unimportant for this question
}
public MenueVM(string header, string content)
{
Header = header;
Content = content;
}
private string _header;
public string Header
{
get
{
return _header;
}
set
{
_header = value;
}
}
Still time to post an answer?
Try this :
XAML:
<TabControl ItemsSource="{Binding....}" IsSynchronizedWithCurrentItem="True">
<!-- style, template, ... -->
</TabControl>
CS:
//Adding your viewModel to your ObservableCollection<> TabControlViews
TabControlViews.Add(_viewModelToAdd);
ICollectionView collectionView = CollectionViewSource.GetDefaultView(TabControlViews);
if (collectionView != null)
{
collectionView.MoveCurrentTo(_viewModelToAdd);
//And this is because you don't want your tabItem to be selected :
collectionView.MoveCurrentToPrevious();
}
Found in the downloadable DemoMVVMApp here : https://msdn.microsoft.com/en-us/magazine/dd419663.aspx#id0090030
I've also spent a huge amount of time to solve this problem... ;-)
The problem is that your tab is been created, but it´s not been selected. So, in addition to calling
TabControlViews.Add(_dataVM)
, you should also update your CurrentTabItem
CurrentTabItem = _dataVM;
and bind your TabControl SelectedItem property to your CurrentTabItem, like this:
<TabControl ItemsSource="{Binding TabControlViews}" SelectedItem="{Binding CurrentTabItem}">
Also, if you remove a TabItem and want to get back to the last one, you have to call
CurrentTabItem = TabControlViews.LastOrDefault();

WPF Binding with Notifyi does not update UI

My issue is that UI is not updated even when PropertyChanged is fired.
XAML:
<ListBox Name="BookShelf" Visibility="Hidden" SelectedItem="{Binding SelectedItem}" Panel.ZIndex="1" Height="Auto" Grid.Column="3" Margin="8,50,0,0" HorizontalAlignment="Center" ItemsSource="{Binding BookShelf}" Background="Transparent" Foreground="Transparent" BorderThickness="0" BorderBrush="#00000000">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel VerticalAlignment="Center" Orientation="Vertical">
<TextBlock FontSize="14" Margin="0,10,0,0" FontWeight="Bold" Foreground="Black" HorizontalAlignment="Center" Text="{Binding Path=DbId}" />
<TextBlock FontSize="16" FontWeight="Bold" Width="170" TextWrapping="Wrap" Foreground="Black" Margin="5" HorizontalAlignment="Center" Text="{Binding Path=DisplayName}" />
<Image HorizontalAlignment="Center" Source="{Binding Path=bookImage}" Width="200" Height="200" Margin="0,0,0,10" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
And:
<ComboBox Margin="8,15,0,0" Name="bookShelf_ComboBox" ItemsSource="{Binding BookShelf}" SelectedItem="{Binding SelectedItem}" VerticalAlignment="Center" HorizontalAlignment="Center" DisplayMemberPath="DisplayName" Height="22" Width="140" Visibility="Visible" SelectionChanged="bookShelf_ComboBox_SelectionChanged"/>
Viewmodel:
public class BookShelfViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public event ShowContentInBrowser ShowDatabaseContent;
public BookShelfViewModel(ShowContentInBrowser showMethod)
{
ShowDatabaseContent += showMethod;
}
private ObservableCollection<DbInfo> _BookShelf = new ObservableCollection<DbInfo>();
public ObservableCollection<DbInfo> BookShelf
{
get
{
if (_BookShelf == null)
_BookShelf = new ObservableCollection<DbInfo>();
return _BookShelf;
}
set
{
if (value != _BookShelf)
_BookShelf = value;
}
}
private DbInfo _selectedItem { get; set; }
public DbInfo SelectedItem
{
get
{
return _selectedItem;
}
set
{
if (_selectedItem != value)
{
_selectedItem = value;
RaisePropertyChanged(new PropertyChangedEventArgs("SelectedItem"));
if (_selectedItem == null)
return;
if (_selectedItem.RelatedId != null)
ShowDatabaseContent(_selectedItem, _selectedItem.RelatedId);
else
ShowDatabaseContent(_selectedItem, _selectedItem.RelatedId);
}
}
}
public void RaisePropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
}
This code I'm using is for setting DataContext and SelectedItem:
await System.Windows.Application.Current.Dispatcher.BeginInvoke(
DispatcherPriority.Background, new Action(
() => this.BookShelf.SelectedItem = dbInfo
)
);
And DataContext:
await System.Windows.Application.Current.Dispatcher.BeginInvoke(
DispatcherPriority.Background, new Action(
() => this.BookShelf.DataContext = bookShelfViewModel
)
);
I'm very new for this MVVM design and as far as I have can tell from articles I have read, I cant find what's wrong. I'm guessing that using Dispatcher is not necessary but I don't think it matters in this case...
ListBox does show my objects but updating SelectedItem is the issue here...
UPDATE:
Heres my code for DbInfo:
public class DbInfo
{
public int RelatedId { get; set; }
public string DbId { get; set; }
public TBase3.Article currentArticle { get; set; }
public string LinkId { get; set; }
public bool IsArticle { get; set; }
public string folder { get; set; }
public bool IsNamedArticle { get; set; }
public int currentBlockIndex { get; set; }
public int currentBlockCount { get; set; }
public string DisplayName { get; set; }
public int VScrollPos { get; set; }
public int THTextVersion { get; set; }
public bool isHtmlToc { get; set; }
public ImageSource bookImage { get; set; }
}
Reminder that when ever I set new value for ViewModel -> SelectedItem and It goes to PropertyChanged(this, e); line. It does not Selected that DbInfo as Selected in ListBox.
Update2:
I got right side of my window a list of books, like a Book Shelf many books in it.
It shows all book with scroll. Book which is selected its content is being shown in window.
But If for reason I want to change to another book from code-behind, it updates it content to webbrowser but not update ListBox that certain book as SelectedItem
ANSWER:
Okay I found the answer right now. Code which set BookShelf.SelectedItem = dbInfo should be bookShelfViewModel.SelectedItem = bookShelfViewModel.BookShelf.First(x => x.DbId == dbInfo.DbIf);
await System.Windows.Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => this.BookShelf.DataContext = bookShelfViewModel));
That does not look good, where do you do this? I would reccomend the use of Galasoft MVVM Light and the usage of a ViewModelLocator, for setting your DataContext(avaliable through nuget).It provides you with a ViewModelBase, with all your propertychanged needs and the works, which you may extend to suit your neeeds. It sounds like the DataContext, may be the actual problem if PropertyChanged isn't even raised.
EDIT:
As pointed out by Clemens the Mode=TwoWay in the binding is not needed here, as it is the default for the SelectedItem property anyway, I'll just leave it as an example....
<ComboBox Margin="8,15,0,0" Name="bookShelf_ComboBox" ItemsSource="{Binding BookShelf}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}" VerticalAlignment="Center" HorizontalAlignment="Center" DisplayMemberPath="DisplayName" Height="22" Width="140" Visibility="Visible">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<command:EventToCommand Command="{Binding SelectedItemChangedCommand}" PassEventArgsToCommand="True"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ComboBox>
I noticed your SelectionChanged="bookShelf_ComboBox_SelectionChanged" in the code, this is "nasty", use event to command instead, because you are tying your viewmodel to your views cbox. The code above will execute a command with arguments included, add the following to your viewmodel.
public ICommand SelectedItemChangedCommand{ get; set;}
// somewhere in your code..
SelectedItemChangedCommand = new RelayCommand<SelectedItemChangedCommand>(OnSelectedItemChanged);
protected void OnSelectedItemChanged(SelectedItemChangedCommand e)
{
// Do your magic here! Or why not just call this method from the setter of your bound property?
}
The WPF Cheat Sheet is a nice compact list of all types of bindings, which is very handy, I used to have it both at wall and home, when I was learning WPF :)
Hope it helps
Cheers
Stian
what you mean by the UI is not updated? you set a new ItemsSource and dont see any Changes?
if that is the case change your Property to
public ObservableCollection<DbInfo> BookShelf
{
get
{
if (_BookShelf == null)
_BookShelf = new ObservableCollection<DbInfo>();
return _BookShelf;
}
set
{
if (value != _BookShelf)
{ _BookShelf = value;
RaisePropertyChanged(new PropertyChangedEventArgs("BookShelf"));
}
}
btw i use ObservableCollection in another way. i just initialize it once and use Clear, Add, Remove.
If the ItemsSource is not your Problem pls post the Code for DbInfo, and write something more to your "UI is not updated" problem
Okay I found the answer right now. Code which set BookShelf.SelectedItem = dbInfo should be bookShelfViewModel.SelectedItem = bookShelfViewModel.BookShelf.First(x => x.DbId == dbInfo.DbIf);

Categories

Resources