Dynamic controls switching in WPF - c#

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.

Related

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.

Property binding to a Child UserControl

I've got a problem I couldn't get solved until now:
I am developing an application in C#/WPF and am using the Caliburn.micro as framework. I have multiple menu panels (as user controls) that I want to reuse all over the application (e.g. data filtering menu for a grid) and show in a <ContentControl />. Depending on the state of the application a different menu panel can be shown.
Now I could get managed to let events bubble up from the menu's View to the parent's ViewModel. But I'm stuck with properties:
For example in the filtering menu, one should enter a text while the filter is instantly applied. When I had the menu in the parent's View it was easy: I just made the filtering in the property's setter method.
Is there a possibility to make a kind of "property-bubbling" similar to the message bubbling in c.m (it has to be twoWay!)? Or any other (better) MVVM-compliant approach?
Thanks in advance!
Jan
Minimal example:
ParentView.xaml
<UserControl x:Class="App.ParentView">
<Grid>
<ContentControl x:Name="Toolbar" />
</Grid>
</UserControl>
ParentViewModel.cs
class ParentViewModel : Screen
{
public ParentViewModel()
{
Toolbar = new MenuViewModel();
}
private Screen _toolbar;
public Screen Toolbar
{
// get, set ...
}
public void LoadDifferentMenu()
{
this.Toolbar = new DifferentMenuViewModel();
}
}
MenuView.xaml
<UserControl x:Class="App.MenuView">
<Grid>
<TextBox x:Name="MyText" />
</Grid>
</UserControl>
MenuViewModel.cs
class MenuViewModel : Screen
{
public MenuViewModel()
{
}
private string _myText;
public string MyText
{
// get, set...
}
}
Use Event Aggregator in caliburn micro to implement publisher and subscriber pattern in MVVM.
Communication is based on message type so it can be used for one way or two way communication with appropriate types.
Kindly refer to the link https://caliburnmicro.com/documentation/event-aggregator for implementation details.

C# referencing a Grid in WPF to change properties

Hello im new to making apps with WPF and XAML in Visual Studio. So I have a grid I want to change its properties in the code.
My Grid's properties:
<Grid HorizontalAlignment="Left"
Height="603"
Margin="0,51,0,0"
x:Name="usersPan"
VerticalAlignment="Top"
Width="1286">
How I have been trying to change its properties
this.usersPan.SetValue(Grid.WidthProperty, PAN_SIZE);
usersPan.SetValue(Grid.WidthProperty, PAN_SIZE);
usersPan.Width = 0;
usersPan.Visibility = Visibility.Collapsed;
When I try to do that^ it says null reference for userPan
Thanks
Noooooooo, Don't ever do that. Make a ViewModel that is bound to the Grid's Width property, and then just change the value.
My suspicion is that you do not need this at all. Have a look into containers, and how to position them.
In all of this years, there have been rare occasions I needed to do that and I suspect you do not need to. Tell me what you are doing.
EDIT:
You have a VM which needs to implement the NotifyPropertyChanged interface (I won't do that here, there are plenty of examples on hoew to do that)
public class MainVM
{
public ObservableCollection<TabVM> TabsVms {get;set;}
public int SelectedIndex {get;set}
}
bound to the control
<TabControl DataContext={TabsVMs} SelectedIndex="{Binding SelectedIndex}">
...
</TabControl>
And in runtime you create a couple of Tabs
var TabsVMs = new ObservableCollection<TabVM>();
TabsVMs.add(new TabVM());
TabsVMs.add(new TabVM());
TabsVMs.add(new TabVM());
Then in runtime you change the value of the index.
MainVm.SelectedIndex = 1
and the the coresponding tab will become selected.
EDIT:
I can also recommend you to use Fody for the MVVM notification.
Also, when it comes to bindings, I can recommend you to use WPF inspector. a handy little tool
The best way to write WPF programs is to use the MVVM (Model-View-View Model) design pattern. There are two (2) ideas behind MVVM:
Write as little code as possible in the view's code-behind and put all of the logic in the View Model object, using WPF's data binding feature to connect the properties of the View Model object to the view's controls.
Separate the logic from the display so you can replace the view with some other construct without having to change the logic.
MVVM is a huge topic on its own. There are lots of articles about it, and frameworks that you can use to build your program. Check out MVVM Light, for example.
Don't know exactly why Grid is invisible in code-behind, but You can access it's properties using events (but don't think it is perfect solution).
For example add to your grid event Loaded
<Grid HorizontalAlignment="Left"
Height="603"
Margin="0,51,0,0"
x:Name="usersPan"
VerticalAlignment="Top"
Width="1286"
Loaded="FrameworkElement_OnLoaded">
and then from code-behind you can access grid in next way:
private void FrameworkElement_OnLoaded(object sender, RoutedEventArgs e)
{
var grid = sender as Grid;
if (grid != null)
{
grid.Width = 0;
}
}
Better solution :
Add some boolean property to your ViewModel like public bool IsGridVisible{get;set;}
And bind it to your Grid
<Grid HorizontalAlignment="Left"
Height="603"
Margin="0,51,0,0"
x:Name="usersPan"
VerticalAlignment="Top"
Width="1286"
Visibility="{Binding Path=IsGridVisible, Converter={StaticResource BoolToVis}">
where BoolToVis is converter which converts true to Visible and false to Hidden. You can define it in App.xaml like :
<BooleanToVisibilityConverter x:Key="BoolToVis" />
I was able to do something like this so I can change properties outside of an event.
private Grid userGrid;
private void onUserGridLoaded(object sender, RoutedEventArgs e)
{
userGrid = sender as Grid;
}

Is there a way to set DataTemplate for a control in an user control (a composite one) in XAML

I have created a composite user control containing a Toolbar and a Datagrid, and expose them as public properties. Is there a way to add new button to Toolbar and set a DataTemplate for Datagrid in XAML, instead of implementing them in the code-behind file if I use this user control in another Window or user control?
I found a similar link here, but has no idea how to do it. Please help.
Here is the Xaml:
<UserControl x:Class="CRUDDataGrid1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" >
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ToolBarTray Grid.Row="0" >
<ToolBar x:Name="tb">
<Button x:Name="Add" Content="Add">
</Button>
</ToolBar>
</ToolBarTray>
<DataGrid Grid.Row="1" x:Name="dg">
</DataGrid>
</Grid>
</UserControl>
And here is the code-behind:
public partial class CRUDDataGrid1 : UserControl
{
public ToolBar ToolBar { get; set; }
public DataGrid DataGrid { get; set; }
public ObservableCollection<DataGridColumn> Columns { get; private set; } //edited
public CRUDDataGrid1()
{
InitializeComponent();
ToolBar = tb;
DataGrid = dg;
Columns = dg.Columns; //edited
}
}
And I want to use this user control in another user control like this:
<UserControl x:Class="UserControl1" ...>
<Grid>
<local:CRUDDataGrid1>
<local:CRUDDataGrid1.ToolBar>
<Button x:Name="Delete" Content="Delete">
</Button>
</local:CRUDDataGrid1.ToolBar>
<local:CRUDDataGrid1.DataGrid ItemsSource="{Binding Customers}">
<local:CRUDDataGrid1.Columns>
<DataGridTextColumn Header="First Name" Binding="{Binding XPath=#FirstName}" />
<DataGridTextColumn Header="Last Name" Binding="{Binding XPath=#LastName}" />
<local:CRUDDataGrid1.Columns>
</local:CRUDDataGrid1.DataGrid>
</local:CRUDDataGrid1>
</Grid>
</UserControl>
1 Foreword
Having a child control which owns a ToolBar and wanting a parent of that child control to add toolbar items to the ToolBar owned by the child is a tell-tale sign of bad
design. The primary and most important advice for you is to rethink your software design to avoid this kind of shared/split initialization.
In almost any scenario, you want the toolbar owned by the top-most control such as the main window, or a document window (in case your application has MDI or floating windows).
The toolbar items would be gathered from the respective controls housed within that window; for example, copy/paste/etc. actions from the document editor control, actions for creating or loading a new document from somewhere else, etc.
Side note: Often, such a design happens because novice WPF programmers want to realize button actions in the old-fashioned way of using Click event handlers. Such Click-event
handlers create code dependencies, and as long as they can be contained within just one (custom) control everything is fine. However, as soon as this is not feasible any more
(for example when an action should appear as a toolbar button or the same action should be triggered through a menu), trying to stick with Click event handlers will lead to convoluted code even for simple UIs and can cause severe headache...
The mechanism in WPF to avoid those pesky Click event handlers are Commands, or more specifically RoutedCommands. To be fair, it has to be noted that RoutedCommands have their own
share of challenges. However, many fine folks wrote many interesting and important things about using WPF's RoutedCommands and how to expand beyond their functionality, so the
only sane advice i can give here is to use the powers of Google if you want/need to know more.
2 Answering the question, but not solving the underlying design issue
To create a ToolBar which has collections of toolbar items defined at different places, while using multiple toolbar bands in the same ToolBarTray is not desired, the toolbar item collections need to be merged into a single list at some point. This can either be done somehow in code-behind, or it can be done in XAML with the help of a custom IMultiValueConverter.
The custom IMultiValueConverter - let's call it MergeCollectionsConverter - will be
agnostic to any data type. It just takes a number of IEnumerables and adds all their elements to the result list. It even accepts objects which are not IEnumerable, those objects themselves will be added to the result list.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows.Data;
using System.Windows.Documents;
namespace MyStuff
{
public class MergeCollectionsConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values == null) return null;
List<object> combinedList = new List<object>();
foreach (object o in values)
{
if (o is IEnumerable)
combinedList.AddRange( ((IEnumerable) o).Cast<object>() );
else
combinedList.Add(o);
}
return combinedList;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
I assume furthermore that the ToolBar inside of CRUDDataGrid1 should be fed from two toolbar item collections. The first collection with the default toolbar items is defined within CRUDDataGrid1. The second collection should allow other controls to append additional toolbar items after the default items; this collection therefore has to be publicly accessible.
Based on your example code from the question, your CRUDDataGrid1 class could look like the following (just considering the toolbar, it does not represent the complete class by any means):
CRUDDataGrid1.cs:
public partial class CRUDDataGrid1 : UserControl, INotifyPropertyChanged
{
public ObservableCollection<object> AdditionalToolbarItems { get { return _additionalToolbarItems; } }
private readonly ObservableCollection<object> _additionalToolbarItems = new ObservableCollection<object>();
public event PropertyChangedEventHandler PropertyChanged;
public UserControl1()
{
InitializeComponent();
_additionalToolbarItems.CollectionChanged +=
(sender, eventArgs) =>
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs("AdditionalToolbarItems"));
};
...other constructor code...
}
}
CRUDDataGrid1.xaml:
<DockPanel>
<ToolBarTray DockPanel.Dock="Top">
<ToolBar>
<ToolBar.Resources>
<DataTemplate DataType="{x:Type My:UseCommand}">
<Button
Style="{StaticResource {x:Static ToolBar.ButtonStyleKey}}"
Command="{Binding Command}"
CommandTarget="{Binding Target}"
CommandParameter="{Binding Parameter}"
Content="{Binding Command.Text}"
/>
</DataTemplate>
<My:MergeCollectionsConverter x:Key="convToolbarItems" />
<x:Array x:Key="defaultToolbarItems" Type="{x:Type sys:Object}">
<My:UseCommand Command="ApplicationCommands.New" />
<My:UseCommand Command="ApplicationCommands.Cut" />
<My:UseCommand Command="ApplicationCommands.Paste" />
</x:Array>
</ToolBar.Resources>
<ToolBar.ItemsSource>
<MultiBinding Converter="{StaticResource convToolbarItems}">
<Binding Source="{StaticResource defaultToolbarItems}" />
<Binding Path="AdditionalToolbarItems" ElementName="crudDataGrid1" />
</MultiBinding>
</ToolBar.ItemsSource>
</ToolBar>
</ToolBarTray>
<DataGrid x:Name="dg" />
</DockPanel>
The first collection is a 'static resource' in the ToolBar's resource directory,
identified by the resource key "defaultToolbarItems". The second is the collection provided by CRUDDataGrid1's property AdditionalToolbarItems. Using a <MultiBinding>
with the aforementioned converter, the merged list is bound to the ToolBar's ItemsSource.
Looking at the C# source code of the AdditionalToolbarItems property, you will notice the INotifyPropertyChanged implementation and the handler for the CollectionChanged
event. Why is that? Remember, that AdditionalToolbarItems is a read-only property. At the time the CRUDDataGrid1 control has been fully constructed, the data binding has been
set and AdditionalToolbarItems been processed by the multi binding. And it would never be
processed again, since the property itself will never change its value (it will
always refer to the same ObservableCollection). To make the <MultiBinding> re-evaluate the bound properties whenever the content of the AdditionalToolbarItems collection has
changed, the code needs to listen for CollectionChanged events and fire an
PropertyChanged event whenever the content of AdditionalToolbarItems has changed, which in turn will cause the <MultiBinding> to re-evaluate the bound properties.
You will also note the usage of <My:UseCommand> elements instead of using <Button>. Well, you could use <Button>, and it would work as well. Until your application wants to use multiple ToolBars at once sharing the same default buttons - in which case you have a problem: A button is a control and thus has one parent UI element. You cannot share
a button control amongst several toolbars, because a control can only be owned as a child by one parent UI element. Thus, RoutedCommands are used instead of button controls
(another, equally important reason will become obvious if you read the 'real' solution in section 3 below). Still, technically nothing would stop you from declaring <Button>
elements -- you could even mix <My:UseCommand> with <Button> (and other elements, as long as those can be rendered in the toolbar).
UseCommand is a pretty small and simple class which allows you to tell which command to use (plus optional CommandTarget and CommandParameter, if required):
namespace MyStuff
{
public class UseCommand
{
public System.Windows.Input.ICommand Command { get; set; }
public System.Windows.IInputElement Target { get; set; }
public object Parameter { get; set; }
}
}
The ToolBar will need a DataTemplate to properly display the command and its parameters stored inside UseCommand. You can see this DataTemplate as part of the ToolBar
resource dictionary in the XAML code above.
With these things in place, using CRUDDataGrid1 in UserControl1 and adding additional toolbar items could look like this:
<UserControl x:Class="UserControl1" ...>
<Grid>
<local:CRUDDataGrid1>
<local:CRUDDataGrid1.AdditionalToolbarItems x:Name="cdg">
<My:UseCommand Command="ApplicationCommands.Close" CommandTarget="{Binding ElementName=cdg}" />
<My:UseCommand Command="ApplicationCommands.New" CommandTarget="{Binding ElementName=cdg}" />
</local:CRUDDataGrid1.AdditionalToolbarItems>
...
</local:CRUDDataGrid1.DataGrid>
</Grid>
</UserControl>
For my example code, i used commands provided by System.Windows.Input.ApplicationCommands. You can ofcourse roll your own commands (as we will see below). Also note the demonstrated usage of the CommandTarget property. Whether using this property is necessary requires some understanding of how RoutedCommands work and mostly depends on where which element in the UI's visual/logical tree has established handlers for that particular command.
3 Using RoutedCommands to solve the design issue and the question
Having read section 2, you should already gotten the idea that RoutedCommands will help you to separate the provision of user-invokable actions by whatever component from the actual UI representation, and that this can help you avoiding the shenanigans about the somewhat convoluted composition of the ToolBar from different sources. Because, all that CRUDDataGrid1 essentially needs to provide for your GUI are the commands for a toolbar (or a menu, or any other command invokers for that matter).
From what i can glance from your source code, CRUDDataGrid1 is responsible for executing the "Add" action, whereas UserControl1 is responsible the "Delete" action.
Both actions should appear in the same toolbar.
Let's look at the "Add" action of CRUDDataGrid1. First and foremost, to make this action invokable through a RoutedCommand, an appropriate RoutedCommand object needs to be provided, obviously. You might choose one of the RoutedCommands provided by .NET (as declared in ApplicationCommands, ComponentCommand and NavigationCommand).
However, this is not always a good idea. Common commands such as ApplicationCommands.Copy can be executed by pretty much any control which supports clipboard operations, and knowing which actual control will handle the invocation of such a command requires knowing about how RoutedCommands are routed through the visual tree and how the logcial focus affects this routing. Thus, sometimes it is
easier to define your own RoutedCommand as a public static property - which we will do here for the "Add" action:
public partial class CRUDDataGrid1 : UserControl
{
public static readonly RoutedCommand AddCommand = new RoutedCommand("CRUDDataGridCommand.Add", typeof(CRUDDataGrid1));
public UserControl1()
{
InitializeComponent();
CommandBindings.Add(
new CommandBinding(
AddCommand,
OnExecutedAddCommand,
CanExecuteAddCommand
)
);
...other constructor code...
}
private void CanExecuteAddCommand(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = ...here your code that decides whether the "Add" command can execute
(and thus whether any button which uses this command will be enabled/disabled)
}
private void CanExecuteAddCommand(object sender, ExecutedRoutedEventArgs e)
{
...execute the "Add" action here...
}
}
Note the command-binding in the constructor as well as the respective methods handling the command. Just to avoid confusion: It is not required that the object serving as CommandTarget has to implement command-bindings. CommandTarget merely specifies the object in the visual/logical tree at which the routing starts.
While i do not show it here, the implementation regarding the DeleteCommand in UserControl is following the same pattern.
public partial class UserControl1 : UserControl
{
public static readonly RoutedCommand DeleteCommand = new RoutedCommand("UserControl1Command.Delete", typeof(UserControl1));
...same implementation approach as demonstrated for CRUDDataGrid1.AddCommand...
}
Creating the ToolBar can now happen entirely in UserControl1.xaml without worrying how the respective actions represented by the commands are executed. Note, that it is fine to use <Button> since the toolbar is entirely created in UserControl1 without a possibility that any of these buttons could be "shared" with another control. Also, note the absence of those helper classes like UseCommand and MergeCollectionsConverter which were required for the somewhat convoluted scenario in section 2 of my answer.
<UserControl x:Class="UserControl1" ...>
<DockPanel>
<ToolBarTray DockPanel.Dock="Top">
<ToolBar>
<Button Content="Add" Command="{x:Static local:CRUDDataGrid1.AddCommand}" CommandTarget="{Binding ElementName=cdg}" />
<Button Content="Delete" Command="{x:Static local:UserControl1.DeleteCommand}" />
</ToolBar>
</ToolBarTray>
<local:CRUDDataGrid1 x:Name="cdg" ItemsSource="{Binding Customers}">
<local:CRUDDataGrid1.Columns>
<DataGridTextColumn Header="First Name" Binding="{Binding XPath=#FirstName}" />
<DataGridTextColumn Header="Last Name" Binding="{Binding XPath=#LastName}" />
</local:CRUDDataGrid1.Columns>
</local:CRUDDataGrid1>
</DockPanel>
</UserControl>
CRUDataGrid1 should probably directly inherit from the DataGrid type (not being a UserControl), implementing the extended CRUD functionality as you require.
By letting CRUDataGrid1 provide only RoutedCommands for any desired user-action, you and anybody else in your team is free in the decision where in the GUI to use the RoutedCommands - in tool bars, in menus or whereever else. You can use multiple buttons using the same command - no problem there. The infrastructure behind RoutedCommands also will take care about automatically enabling/disabling such buttons depending on the result of the CanExecute method bound to a command.
In the example given here, i did let CRUDataGrid1 and UserControl1 provide the RoutedCommands. But if you have many commands and more complex software, then there is nothing speaking against having a central place for defining those commands (similar to what Microsoft did with the RoutedCommands provided by the .NET framework).

C# User Control that can contain other Controls (when using it)

I found something about this issue for ASP, but it didn't help me much ...
What I'd like to do is the following: I want to create a user control that has a collection as property and buttons to navigate through this collection. I want to be able to bind this user control to a collection and display different controls on it (containing data from that collection).
Like what you had in MS Access on the lower edge of a form ...
to be more precise:
When I actually use the control in my application (after I created it), I want to be able to add multiple controls to it (textboxes, labels etc) between the <myControly> and </mycontrol>
If I do that now, the controls on my user control disappear.
Here is an example of one way to do what you want:
First, the code - UserControl1.xaml.cs
public partial class UserControl1 : UserControl
{
public static readonly DependencyProperty MyContentProperty =
DependencyProperty.Register("MyContent", typeof(object), typeof(UserControl1));
public UserControl1()
{
InitializeComponent();
}
public object MyContent
{
get { return GetValue(MyContentProperty); }
set { SetValue(MyContentProperty, value); }
}
}
And the user control's XAML - UserControl1.xaml
<UserControl x:Class="InCtrl.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="300" Width="300" Name="MyCtrl">
<StackPanel>
<Button Content="Up"/>
<ContentPresenter Content="{Binding ElementName=MyCtrl, Path=MyContent}"/>
<Button Content="Down"/>
</StackPanel>
</UserControl>
And finally, the xaml to use our wonderful new control:
<Window x:Class="InCtrl.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:me="clr-namespace:InCtrl"
Title="Window1" Height="300" Width="300">
<Grid>
<me:UserControl1>
<me:UserControl1.MyContent>
<Button Content="Middle"/>
</me:UserControl1.MyContent>
</me:UserControl1>
</Grid>
</Window>
I'm having a hard time understanding your question, but I think what you're describing is an ItemsControl using DataTemplates to display the contents of (presumably) an ObservableCollection(T).
A UserControl may not be the best way to do this. You're wanting to add decorations around content, which is basically what Border does: it has a child element, and it adds its own stuff around the edges.
Look into the Decorator class, which Border descends from. If you make your own Border descendant, you should be easily able to do what you want. However, I believe this would require writing code, not XAML.
You might still want to make a UserControl to wrap the buttons at the bottom, just so you can use the visual designer for part of the process. But Decorator would be a good way to glue the pieces together and allow for user-definable content.
Here's a link to a built-in control (HeaderedContentControl) that does the same thing as the accepted answer except that it is an existing control in WPF since .Net 3.0

Categories

Resources