Search code examples
c#wpfmvvmgridtabitem

Trouble copying a Grid object from one TabItem to another


In my program I have tabItems that have their commands bound to a View Model. I am in the process of implementing a function that will copy the design structure of a "master" tabItem, along with it's command functionality in order to create a new tabItem. I need to do this because the user of this program will be allowed to add new tabItems.

Currently I am using the question Copying a TabItem with an MVVM structure, but I seem to be having trouble when the function tries to copy the Grid object using dependencyValue.

The class I am using:

public static class copyTabItems
{
    public static IList<DependencyProperty> GetAllProperties(DependencyObject obj)
    {
        return (from PropertyDescriptor pd in TypeDescriptor.GetProperties(obj, new Attribute[] { new PropertyFilterAttribute(PropertyFilterOptions.SetValues) })
                    select DependencyPropertyDescriptor.FromProperty(pd)
                    into dpd
                    where dpd != null
                    select dpd.DependencyProperty).ToList();
    }

    public static void CopyPropertiesFrom(this FrameworkElement controlToSet,
                                                   FrameworkElement controlToCopy)
    {
        foreach (var dependencyValue in GetAllProperties(controlToCopy)
                .Where((item) => !item.ReadOnly)
                .ToDictionary(dependencyProperty => dependencyProperty, controlToCopy.GetValue))
        {
            controlToSet.SetValue(dependencyValue.Key, dependencyValue.Value);
        }
    }
}

When dependencyValue gets to {[Content, System.Windows.Controls.Grid]} the program throws an InvalidOperationException was Unhandled stating that, "Specified element is already the logical child of another element. Disconnect it first".

What does this mean? Is this a common problem with the Grid in WPF (am I breaking some rule by trying to do this?)? Is there something in my program that I am not aware of that is causing this?


Solution

  • Ok. This is how you're supposed to deal with a TabControl in WPF:

    <Window x:Class="MiscSamples.MVVMTabControlSample"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:MiscSamples"
            Title="MVVMTabControlSample" Height="300" Width="300">
        <Window.Resources>
            <DataTemplate DataType="{x:Type local:Tab1ViewModel}">
                <!-- Here I just put UI elements and DataBinding -->
                <!-- You may want to encapsulate these into separate UserControls or something -->
                <StackPanel>
                    <TextBlock Text="This is Tab1ViewModel!!"/>
                    <TextBlock Text="Text1:"/>
                    <TextBox Text="{Binding Text1}"/>
                    <TextBlock Text="Text2:"/>
                    <TextBox Text="{Binding Text2}"/>
                    <CheckBox IsChecked="{Binding MyBoolean}"/>
                    <Button Command="{Binding MyCommand}" Content="My Command!"/>
                </StackPanel>
            </DataTemplate>
    
            <!-- Here you would add additional DataTemplates for each different Tab type (where UI and logic is different from Tab 1) -->
        </Window.Resources>
    
        <DockPanel>
            <Button Command="{Binding AddNewTabCommand}" Content="AddNewTab"
                    DockPanel.Dock="Bottom"/>
    
            <TabControl ItemsSource="{Binding Tabs}"
                        SelectedItem="{Binding SelectedTab}"
                        DisplayMemberPath="Title">
    
            </TabControl>
        </DockPanel>
    </Window>
    

    Code Behind:

    public partial class MVVMTabControlSample : Window
    {
        public MVVMTabControlSample()
        {
            InitializeComponent();
    
            DataContext = new MVVMTabControlViewModel();
        }
    }
    

    Main ViewModel:

    public class MVVMTabControlViewModel: PropertyChangedBase
    {
        public ObservableCollection<MVVMTabItemViewModel> Tabs { get; set; }
    
        private MVVMTabItemViewModel _selectedTab;
        public MVVMTabItemViewModel SelectedTab
        {
            get { return _selectedTab; }
            set
            {
                _selectedTab = value;
                OnPropertyChanged("SelectedTab");
            }
        }
    
        public Command AddNewTabCommand { get; set; }
    
        public MVVMTabControlViewModel()
        {
            Tabs = new ObservableCollection<MVVMTabItemViewModel>();
            AddNewTabCommand = new Command(AddNewTab);
        }
    
        private void AddNewTab()
        {
            //Here I just create a new instance of TabViewModel
            //If you want to copy the **Data** from a previous tab or something you need to 
            //copy the property values from the previously selected ViewModel or whatever.
    
            var newtab = new Tab1ViewModel {Title = "Tab #" + (Tabs.Count + 1)};
            Tabs.Add(newtab);
    
            SelectedTab = newtab;
        }
    }
    

    Abstract TabItem ViewModel (you to derive from this to create each different Tab "Widget")

    public abstract class MVVMTabItemViewModel: PropertyChangedBase
    {
        public string Title { get; set; }
    
        //Here you may want to add additional properties and logic common to ALL tab types.
    }
    

    TabItem 1 ViewModel:

    public class Tab1ViewModel: MVVMTabItemViewModel
    {
        private string _text1;
        private string _text2;
        private bool _myBoolean;
    
        public Tab1ViewModel()
        {
            MyCommand = new Command(MyMethod);
        }
    
        public string Text1
        {
            get { return _text1; }
            set
            {
                _text1 = value;
                OnPropertyChanged("Text1");
            }
        }
    
        public bool MyBoolean
        {
            get { return _myBoolean; }
            set
            {
                _myBoolean = value;
                MyCommand.IsEnabled = !value;
            }
        }
    
        public string Text2
        {
            get { return _text2; }
            set
            {
                _text2 = value;
                OnPropertyChanged("Text2");
            }
        }
    
        public Command MyCommand { get; set; }
    
        private void MyMethod()
        {
            Text1 = Text2;
        }
    }
    

    Edit: I forgot to post the Command class (though you surely have your own)

    public class Command : ICommand
    {
        public Action Action { get; set; }
    
        public void Execute(object parameter)
        {
            if (Action != null)
                Action();
        }
    
        public bool CanExecute(object parameter)
        {
            return IsEnabled;
        }
    
        private bool _isEnabled = true;
        public bool IsEnabled
        {
            get { return _isEnabled; }
            set
            {
                _isEnabled = value;
                if (CanExecuteChanged != null)
                    CanExecuteChanged(this, EventArgs.Empty);
            }
        }
    
        public event EventHandler CanExecuteChanged;
    
        public Command(Action action)
        {
            Action = action;
        }
    }
    

    And finally PropertyChangedBase (just a helper class)

        public class PropertyChangedBase:INotifyPropertyChanged
        {
            public event PropertyChangedEventHandler PropertyChanged;
    
            protected virtual void OnPropertyChanged(string propertyName)
            {
                PropertyChangedEventHandler handler = PropertyChanged;
                if (handler != null) 
                   handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    

    Result:

    enter image description here

    • Basically, each Tab Item type is a Widget, which contains its own logic and Data.
    • You define all logic and data at the ViewModel or Model level, and never at the UI level.
    • You manipulate the data defined in either the ViewModel or the Model level, and have the UI updated via DataBinding, never touching the UI directly.
    • Notice How I'm leveraging DataTemplates in order to provide a specific UI for each Tab Item ViewModel class.
    • When copying a new Tab, you just create a new instance of the desired ViewModel, and add it to the ObservableCollection. WPF's DataBinding automatically updates the UI based on the Collection's change notification.
    • If you want to create additional tab types, just derive from MVVMTabItemViewModel and add your logic and data there. Then, you create a DataTemplate for that new ViewModel and WPF takes care of the rest.
    • You never, ever, ever manipulate UI elements in procedural code in WPF, unless there's a REAL reason to do so. You don't "uncheck" or "disable" UI Elements because UI elements MUST reflect the STATE of the data which is provided by the ViewModel. So a "Check/Uncheck" state or an "Enabled/Disabled" state is just a bool property in the ViewModel to which the UI binds.
    • Notice how this completely removes the need for horrendous winforms-like hacks and also removes the need for VisualTreeHelper.ComplicateMyCode() kind of things.
    • Copy and paste my code in a File -> New Project -> WPF Application and see the results for yourself.