Open referenced control in a new window - c#

Issue
I want to load a referenced control from the main window into a new window. The referenced control is already child of the main window, causing the following exception when attempting to render the new window:
System.ArgumentException was unhandled:
Must disconnect specified child from current parent Visual before attaching to new parent Visual.
I do not want to disconnect it from the main window and I also cannot create a new instance of the control since I do not know how it's instantiated or what members are applied.
Background
I'm developing an application that allows developers to extend the application with additional views of configuration options. The container of these views may turn out to be too small for large view extensions (imagine a scheduling control for agendas as example), so I wish to provide the user with the ability to open the extended view in a new window.
Code
So far I've created a behavior to attach to Hyperlinks that opens a new window with the referenced control upon the Click event. The following code is the most basic implementation to demonstrate my intention:
public class ExpandViewBehavior : Behavior<Hyperlink>
{
public static DependencyProperty ViewProperty = DependencyProperty.Register("View", typeof(object), typeof(ExpandViewBehavior));
public object View
{
get { return GetValue(ViewProperty); }
set { SetValue(ViewProperty, value); }
}
protected override void OnAttached()
{
this.AssociatedObject.Click += AssociatedObject_Click;
}
void AssociatedObject_Click(object sender, RoutedEventArgs e)
{
if (View != null)
{
var window = new Window()
{
Content = View
};
window.Show();
}
}
}
Attached to a Hyperlink in the main window, referencing a simple TextBox to load in the new window. Where i is the System.Windows.Interactivity namespace and local my project namespace.
xmlns:local="clr-namespace:WpfApplication"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
...
<StackPanel>
<TextBlock>
<Hyperlink>
<i:Interaction.Behaviors>
<local:ExpandViewBehavior
View="{Binding Source={x:Reference SomeControl}}" />
</i:Interaction.Behaviors>
<TextBlock
Text="(Open in new window)" />
</Hyperlink>
</TextBlock>
<TextBox
x:Name="SomeControl" />
</StackPanel>
My question is, is there is a way to load the referenced control without disconnecting it from the main window?

As you can't display the same control in two places at once, you will either need to disconnect the control from the main window (as noted in the error text), or create a copy of the control to place into the child window.
You can clone the control by exporting its XAML, and then create a new control from this XAML.
See this answer How can you clone a WPF object? for more details.

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.

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.

Attach to a click event in XAML control when loading XAML dynamically.

I have a XAML control which gets loaded dynamically at runtime. This is pure XAML with no code behind.
I dont have any control over the parent loading mechanism which is why this looks a bit weird.
I have a parent application which loads my Plugin DLL and loads my XAML Control.
My DLL signature is:
public class Application : BaseClassHere
{
public Application(IParentContext context) : base(context)
{
// Im placing this instance in the bag which i use in my XAML
base.MyObservablePropertyBag["MyParentContext"] = new ObservableValue<object>(this);
}
}
My XAML is like this:
<av:UserControl
xmlns:local="clr-namespace:MyApplicationNS;assembly=MyDll"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:av="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" >
<Border DataContext="{Binding MyObservablePropertyBag[MyParentContext].Value}" >
<Button Name="MyButton" />
</Border>
</UserControl>
This binding works perfectly in the XAML. I have full access to all public properties that i definein the Application class.
My problem is that i want to link to up Click events on my Buttons. If i add a Click event in the XAML it errors at runtime telling me that i need to compile my XAML.
Is there any way to subscribe to the Click event on MyButton in the Application class?
Well, if you say binding works then why try to mess with the Click event? Rather go with the Button's Command property. Create an ICommand instance (like a DelegateCommand or RelayCommand) that you place in your propertybag and just bind to it!

DataTemplate not used when WPF control is opened within Windows Form Control

I have a DataTemplate that I created to manage the display of the items in a ListBox. When I open the control in a regular WPF application, it works correctly. However, when I open it in a a Windows Form control hosted in a Windows Form application, the datatemplate is not used. Thinking that it may be a problem with "FindResource" in that context, I added the DataTemplate in the code-behind. Once again it worked correctly when opening in a regular WPF app, but failed when opening in the Windows Form app.
If I set the background of the ListBox itself, I see the background as well as blank rows for all of the items that "should" be displayed - so I know the data is getting there, its just that the template does not apply itself.
code to load windows form control:
_elementHost = new ElementHost();
_elementHost.Dock = DockStyle.Fill;
this.Controls.Add(_elementHost);
NavigationControl userControl = new NavigationControl(); // the wpf control
_elementHost.Child = userControl;
dataTemplate xaml:
<DataTemplate x:Key="WorkingAccountResultTemplate" >
<StackPanel Orientation="Horizontal">
<TextBlock Text="Test" Width="50" Foreground="Purple" Background="AliceBlue"/>
<TextBlock Text="{Binding ItemKeyId}" HorizontalAlignment="Stretch" Background="Maroon" />
</StackPanel>
</DataTemplate>
You need to start the WPF instance in your application. In order to do this create an App.xaml file in your startup or main winforms project.
App.xaml, you should have the following lines, in addition to the includes and class declaration: (You can have an empty file with just the includes, so this section can be empty)
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/PresentationFramework.Aero;component/themes/Aero.NormalColor.xaml"/>
</ResourceDictionary.MergedDictionaries>
</Application.Resources>
App.xaml, In the code behind:
public partial class App : Application
{
public App()
{
StyleManager.ApplicationTheme =new Windows7Theme();
InitializeComponent();
}
public static void EnsureApplicationResources()
{
if (Application.Current == null)
{
// create the Application object
new App {ShutdownMode = ShutdownMode.OnExplicitShutdown};
}
}
protected override void OnExit(ExitEventArgs e)
{
if(Current != null)
Current.Shutdown();
base.OnExit(e);
}
}
In the start-up method in Main.cs or Program.cs:
private static void Main()
{
// Your initialization code
//WPF instance start
App.EnsureApplicationResources();
Application.Run(MainForm.Instance);
}

Categories

Resources