WPF ToolBarControl in multiple UserControls - c#

I'm working on a project where the request is to implement ToolBarControl in multiple UserControls.
The UserControl would mostly have that toolbar and GridView (Devexpress).
I'm using WPF with MVVM and Caliburn.Micro framework for development.
The problem is that, that I need to c/p code of the ToolBarControl in the XAML and then in ViewModel to implement the properties.
I'm searching for a better way, and for now I guess it would be reflection.
Any advice would be helpful, code examples too.
Update #2
The controls who would be in the custom toolbar, would should be able to move selected row up or down, delete item, edit and create (last two should open a new window).
Let's say I have CustomersListViewModel, who in it's CustomersListView has the custom ToolBarControl and GridControl.
When I click add button, it should open me CustomersEditViewModel.
When I click delete, it should delete selected item in list.
When I click move up, it should move up selected row.

You could use a datatemplate toolbarviemodel and toolbarview in your app.xaml and then use a contentcontrol to show the toolbar binding it to an instance of your toolbarviewmodel
app.xaml:
<ResourceDictionary>
<DataTemplate DataType="{x:Type ViewModelToolBar}">
<startViews:ViewToolBar />
</DataTemplate>
</ResourceDictionary>
and in your usercontrol:
<ContentControl Content="{Binding MyViewModelToolBar}"/>
and to execute your commands you could use a notify event with a tag or so as parameter to tell your usercontrol viewmodel wich operation should be performed.Means you bind your toolbar buttons to the notifycommand and use button name or tag as parameter.
ViewModelToolBar:
public event EventHandler Notify;
private void OnNotify(object sender)
{
Notify?.Invoke(sender, new EventArgs());
}
public ICommand NotifyCommand => new DelegateCommand<object>(OnNotify);
and in your usercontrol ViewModel:
MyViewModelToolBar = new ViewModelToolBar();
ViewModelToolBar.Notify += ViewModelToolBar_Notify;
private void ViewModelToolBar_Notify(object sender, EventArgs e)
{
switch (sender.ToString())
{
case "Case1":
"perform your operation"
break;
case "Case2":
...
break;
case "Case3":
...
break;
}
}

Related

Dynamic controls switching in WPF

Title might be misleading but i'm not sure how to describe it.
Lets say i have 2 containers - one on the left, one on the right. Left container has multiple buttons. Pressing them will change whats inside 2nd container.
If i press 1st button a set of buttons and calendar will appear, 2nd - datagridview etc. Its example.
How can i achieve it? I'm not asking for solution (it can't be solved in one line of code, obviously), but what should i search for. Some specific control? Displaying other window inside it? Etc.
I am not sure if I understood the question well, so I wrote the following scenario from what I understood.
As you mentioned, you have a main window that contains 2 panels, one on the left and the other on the right. In the left panel, there is a list of buttons placed as a group of menus, which, when clicked, show other content in the right panel, something like a navigation to another system module (see the gif):
If this is your scenario, you can design your WPF application as follows:
Create UserControls for each screen you want to navigate to. In the previous example, you could create a UserControl for the module of the task list, and another UserControl for the module of My Agenda. Check this link so you know what a UserControl is.
Manage navigation on the main window. Just like in WinForms, you could handle the click event on each button in the left panel, however, an elegant way to handle the click event is that your handle it in the parent container, since, unlike Winforms, the click event is a bubbling event. Check this link, so you know what a routed event and what is a bubbling event.
In the example video, could you notice that each module is in a container that has a header and that the header text changes when the button is clicked and the header text is updated with the button text? This can be done in many ways, but a good way to do it is through data binding, check this link to understand what this concept is. With experience, you will realize when it will be advisable to apply this and when it will not.
As you can see, there are many concepts that you should review and learn to be able to make a good design of an application taking advantage of all the benefits that WPF has and to continue with the philosophy of WPF.
I write an example code that I also publish on GitHub. I explain some things about the code, but I suggest that you expand these concepts in the links that I left you and in other reliable sources of knowledge, such as books or tutorials from Microsoft itself.
The Xaml MainWindow:
<Window
x:Class="WpfApp26.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp26"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="800" Height="450"
d:DataContext="{d:DesignInstance Type=local:ViewModel}"
mc:Ignorable="d">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<!-- A GroupBox is a control with a header -->
<GroupBox Header="Options">
<!-- Look that the click event is handled in the StackPanel, the container for the buttons -->
<StackPanel Button.Click="ModuleSelected_OnClick">
<Button
Margin="5" Padding="5"
Content="To Do List" Tag="ToDoListModule" />
<Button
Margin="5" Padding="5"
Content="My Agenda" Tag="MyAgendaModule" />
</StackPanel>
</GroupBox>
<!-- The header property is binding to the CurrentModuleName property in the DataContext -->
<GroupBox Name="GbCurrentModule" Grid.Column="1" Header="{Binding CurretModuleName}" />
</Grid>
</Window>
The MainWindow code behind (review the INotifyProperyChanged):
public partial class MainWindow : Window {
private readonly ViewModel vm;
public MainWindow() {
InitializeComponent();
// Setting the Window's DataContext to a object of the ViewModel class.
this.DataContext = this.vm = new ViewModel();
}
private void ModuleSelected_OnClick(object sender, RoutedEventArgs e) {
// The Source property of the RoutedEventArgs gets the Element that fires the event (in this case, the button).
var clickedButton = (Button) e.Source;
this.vm.CurretModuleName = clickedButton.Content.ToString();
// Getting the Tag property of the button.
var tag = clickedButton.Tag.ToString();
// Performing the navigation.
switch (tag) {
case "ToDoListModule":
NavigateToModule(new UcToDoListModule());
break;
case "MyAgendaModule":
NavigateToModule(new UcMyAgendaModule());
break;
}
#region Internal methods
void NavigateToModule(UserControl uc) {
this.GbCurrentModule.Content = uc;
}
#endregion
}
}
The ViewModel class:
// The class implementents the INotifyPropertyChanged interface, that is used
// by the WPF notifications system.
public class ViewModel : INotifyPropertyChanged {
private string curretModuleName;
public string CurretModuleName {
get => this.curretModuleName;
set {
this.curretModuleName = value;
this.OnPropertyChanged();
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) {
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
You can use DataTemplates with Data Binding: https://learn.microsoft.com/en-us/dotnet/framework/wpf/data/data-templating-overview
This will allow you to define templates that are automatically applied to objects of specific types. So you could have a calendar object, list view, data grid, etc apply individually.
You could also use the visibility to show/hide the view as desired when your button(s) are clicked.
MVVM frameworks use this quite often: https://compiledexperience.com/blog/posts/using-caliburn-micro-as-a-data-template-selector
Another example https://www.codemag.com/article/0907111/Dressing-Up-Your-Data-with-WPF-DataTemplates
There are also other MVVM approaches that use activators to show/hide/generate new objects of specific types and display them.

TabControl caching with code behind layout changes

In regarding with this SO question here, it is posible to update other tab content from code behind and let caching allow to re-cache changed UI elements? Like in scenario I have updated DataGrid scroll index for some tabs on TabControl on some event,
dgvLogs.ScrollIntoView( log );
Now since tab is already cached and above change not reflecting when user switch to tab where dgvLogs located.
EDIT
I have tab control (ExTabControl) in main window and multiple tab holding datagrid which displaying some application logs inside it. Like this:
ExTabControl like this:
<controls:ExTabControl Grid.Row="1" ItemsSource="{Binding Tabs, Mode=OneWay}" >
<controls:ExTabControl.Resources>
<Style TargetType="{x:Type TabItem}">
</Style>
</controls:ExTabControl.Resources>
</controls:ExTabControl>
Single tab having datagrid like this:
<DataGrid Name="dgvLogs" ItemsSource="{Binding Logs}" VerticalScrollBarVisibility="Auto" FrozenColumnCount="4">
Problem:
Lets say I have 3 tab in ExTabControl, selected tab is 1 and from code behind have have update scroll index for tab 2 using dgvLogs.ScrollIntoView( someInbetweenlog );. Ideally if I do select tab 2 then select scroll index inside dgvLogs should be where someInbetweenlog is located. But unfortunately tab 2 scroll not moving as per changes made code behind..
If I do make use of default tab control i.e. TabControl insted of ExTabControl then it is working fine as expected. but if I move scroll in any of tab for dgvLogs then it is reflecting in other tabs also..
Please add comment I'll post more code if required.
EDIT 2
I have created sample application in which I tried to demonstrate the issue. In this app I have added context menu for grid in tab and using Sync option I am trying to scroll to view where first matching log found with closed selected log, in other opened tabs.
Issue: ExTabControl unable to scroll to required log item in different opened tab.
https://github.com/ankushmadankar/StackOverflow54198246/
If I do make use of default tab control i.e. TabControl instead of ExTabControl then it is working fine as expected. But if I move scroll in any of tab for dgvLogs then it is reflecting in other tabs also.
There are two uses of TabControl, extending on this post:
When we bind ItemsSource to a list of items, and we have set the same DataTemplate for each item, TabControl will create only one "Content" view for all items. And when a different tab item is selected, the View doesn't change but the backing DataContext is bound to the viewmodel of the newly selected item.
Is it possible to update other tab content from code behind and let caching allow to re-cache changed UI elements?
The reason the updates won't work is because of another WPF optimization, from UIElement.IsVisible:
Elements where IsVisible is false do not participate in input events (or commands), do not influence either the measure or arrange passes of layout, are not focusable, are not in a tab sequence, and will not be reported in hit testing.
You can change properties on cached elements, but some operations require that an UIElement is visible in order to take effect.
Worth noting:
If you call ScrollIntoView on a DataGrid that's not visible, it won't scroll to the given object. So the ScrollToSelectedBehavior from your linked project is intended to scroll a datagrid that is visible during the process.
In the code of ExTabControl the method UpdateSelectedItem sets the visibility of non active contentpresenters to collapsed.
Given you've explicitly asked for code behind,
A quick hack
TraceViewerView.xaml
<DataGrid IsVisibleChanged="dgvLogs_IsVisibleChanged" ... >
TraceViewerView.xaml.cs
private void dgvLogs_IsVisibleChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
{
if (sender is DataGrid dataGrid && dataGrid.IsVisible)
{
TraceViewerViewModel viewModel = (TraceViewerViewModel)DataContext;
if (viewModel.Log != null)
dataGrid.ScrollIntoView(viewModel.Log);
}
}
A couple of remarks:
You can now remove the line local:ScrollToSelectedBehavior.SelectedValue="{Binding Log}" as we are fetching the sync value straight from the viewmodel.
This is a hack, the view is hard coded to your viewmodel, which is likely to blow up at some time.
A better way
First, to keep our code loosely coupled, an interface.
interface ISync
{
object SyncValue { get; }
}
TraceViewerModel.cs
public class TraceViewerViewModel : PropertyObservable, ITabItem, ISync
Rename Log to SyncValue, and replace the original code
private TraceLog synclog;
public TraceLog Log
{
get { return synclog; }
private set
{
synclog = value;
OnPropertyChanged();
}
}
with
public object SyncValue { get; set; }
Basically, we're trading in a Binding for an interface. The reason I went for the interface in this specific use case, is that you only need to check a tab's sync value when you move to it (making a full fledged Binding a bit overkill).
Next, let's create a Behavior that does what you want.
Instead of an Attached Property I'll use Interactivity Behaviors, which provide a more encapsulated way to extend functionality (requires System.Windows.Interactivity).
ScrollToSyncValueBehavior.cs
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
namespace WpfApp1
{
public class ScrollToSyncValueBehavior : Behavior<DataGrid>
{
protected override void OnAttached()
{
this.AssociatedObject.IsVisibleChanged += OnVisibleChanged;
}
protected override void OnDetaching()
{
this.AssociatedObject.IsVisibleChanged -= OnVisibleChanged;
}
private static void OnVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (sender is DataGrid dataGrid && dataGrid.IsVisible)
{
ISync viewModel = dataGrid.DataContext as ISync;
if (viewModel?.SyncValue != null)
dataGrid.ScrollIntoView(viewModel.SyncValue);
}
}
}
}
TraceViewerView.xaml
<UserControl x:Class="WpfApp1.TraceViewerView"
...
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<DataGrid CanUserAddRows="false" GridLinesVisibility="None" AutoGenerateColumns="False"
ItemsSource="{Binding Logs}">
<i:Interaction.Behaviors>
<local:ScrollToSyncValueBehavior />
</i:Interaction.Behaviors>
...
</DataGrid>
</Grid>
</UserControl>
You need to derive TabControl as described in this answer. Using that technique, the visual tree for each tab will be preserved.
Please note that if you have lots of tabs the caching will imply a significant performance impact. I would recommend using it for at most 10 tabs.

TreeViewItem Command binding

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

How to differ click input between inside and outside of a UserControl?

(Please take each of controls stated below as control created using MVVM pattern)
So, I have a UserControl which I place on my MainWindow. I want my UserControl, if clicked (in the MainWindow, inside the UserControl), the background changed into another color, and if I click in the MainWindow, but outside of UserControl, then the UserControl's background will change to the original color.
What I've tried :
I've tried to apply a Command inside the UserControl.InputBindings which to detect Mouse Input (MouseBinding), but the only MouseBinding raised is the MouseBinding in the Window.InputBindings (which should be raised ONLY when the click input is outside the UserControl), but apparently, wherever a click happen, the only MouseBinding raised is only the one in Window.InputBindings.
Differ the CommandParameter between MouseBinding in Window.InputBindings and UserControl.InputBindings.
Question :
How to differ the MouseBinding between clicking inside the UserControl and outside?
Thanks
The solution is simple. Just attach a PreviewMouseDown event handler to both the Window and the UserControl and handle both events in the Window:
<Window ... PreviewMouseDown="Window_PreviewMouseDown">
<UserControl Name="Control" PreviewMouseDown="UserControl_PreviewMouseDown" ... / >
</Window>
...
private void Window_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
Control.Background = someNewColourBrush;
}
private void UserControl_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
Control.Background = originalColourBrush;
}

WPF Listbox style with a button

I have a ListBox that has a style defined for ListBoxItems. Inside this style, I have some labels and a button. One that button, I want to define a click event that can be handled on my page (or any page that uses that style). How do I create an event handler on my WPF page to handle the event from my ListBoxItems style?
Here is my style (affected code only):
<Style x:Key="UsersTimeOffList" TargetType="{x:Type ListBoxItem}">
...
<Grid>
<Button x:Name="btnRemove" Content="Remove" Margin="0,10,40,0" Click="btnRemove_Click" />
</Grid>
</Style>
Thanks!
Take a look at RoutedCommands.
Define your command in myclass somewhere as follows:
public static readonly RoutedCommand Login = new RoutedCommand();
Now define your button with this command:
<Button Command="{x:Static myclass.Login}" />
You can use CommandParameter for extra information..
Now last but not least, start listening to your command:
In the constructor of the class you wish to do some nice stuff, you place:
CommandBindings.Add(new CommandBinding(myclass.Login, ExecuteLogin));
or in XAML:
<UserControl.CommandBindings>
<CommandBinding Command="{x:Static myclass.Login}" Executed="ExecuteLogin" />
</UserControl.CommandBindings>
And you implement the delegate the CommandBinding needs:
private void ExecuteLogin(object sender, ExecutedRoutedEventArgs e)
{
//Your code goes here... e has your parameter!
}
You can start listening to this command everywhere in your visual tree!
Hope this helps
PS You can also define the CommandBinding with a CanExecute delegate which will even disable your command if the CanExecute says so :)
PPS Here is another example: RoutedCommands in WPF
As Arcturus posted, RoutedCommands are a great way to achieve this. However, if there's only the one button in your DataTemplate then this might be a bit simpler:
You can actually handle any button's Click event from the host ListBox, like this:
<ListBox Button.Click="removeButtonClick" ... />
Any buttons contained within the ListBox will fire that event when they're clicked on. From within the event handler you can use e.OriginalSource to get a reference back to the button that was clicked on.
Obviously this is too simplistic if your ListBoxItems have more than one button, but in many cases it works just fine.
You could create a user control (.ascx) to house the listbox. Then add a public event for the page.
Public Event btnRemove()
Then on the button click event in the usercontrol
RaiseEvent btnRemove()
You can also pass objects through the event just like any other method. This will allow your user control to tell your page what to delete.

Categories

Resources