Проблема с копированием объекта Grid из одного TabItem в другой

В моей программе есть tabItems, команды которых привязаны к модели представления. Я нахожусь в процессе реализации функции, которая будет копировать структуру дизайна «мастера» tabItem вместе с его функциональностью команды для создания нового tabItem. Мне нужно сделать это, потому что пользователю этой программы будет разрешено добавлять новые tabItems.

В настоящее время я использую вопрос Копирование TabItem со структурой MVVM, но у меня, кажется, проблемы, когда функция пытается скопировать объект Grid, используя dependencyValue.

Класс, который я использую:

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);
        }
    }
}

Когда dependencyValue достигает {[Content, System.Windows.Controls.Grid]}, программа выдает InvalidOperationException was Unhandled, утверждая, что "Указанный элемент уже является логическим дочерним элементом другого элемента. Сначала отключите его".

Что это значит? Является ли это общей проблемой с Grid в WPF (я нарушаю какое-то правило, пытаясь это сделать?)? Есть ли что-то в моей программе, о чем я не знаю, что вызывает это?


person Eric after dark    schedule 09.09.2013    source источник
comment
чувак, ты зашел слишком далеко с этими ужасными хаками. Пожалуйста, позвольте мне создать образец для вас.   -  person Federico Berasategui    schedule 09.09.2013
comment
Эй, мои команды для моих элементов XAML теперь находятся в моей модели представления. Я посмотрел на ваш ответ на мой последний вопрос, но я ОЧЕНЬ СОМНЕВАЮСЬ. Это потому, что я не хочу полностью переделывать свой tabItems, это поставит под угрозу всю мою программу. Разве этот класс не делает то, о чем вы говорили? -- скопировать функциональность моего tabItems?   -  person Eric after dark    schedule 09.09.2013
comment
Нет, чувак, когда ты поймешь, что пользовательский интерфейс — это не данные, и что ты не должен даже касаться элементов пользовательского интерфейса в WPF?   -  person Federico Berasategui    schedule 09.09.2013
comment
Итак, как я вообще должен делать простые вещи, такие как снятие флажка или включение объекта? @Rachel сказал мне, что для этого подходит ViewModel, как у меня здесь: stackoverflow.com/questions/18622916/   -  person Eric after dark    schedule 09.09.2013


Ответы (1)


Ok. Вот как вы должны иметь дело с TabControl в 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>

Код позади:

public partial class MVVMTabControlSample : Window
{
    public MVVMTabControlSample()
    {
        InitializeComponent();

        DataContext = new MVVMTabControlViewModel();
    }
}

Основная 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;
    }
}

Абстрактная TabItem ViewModel (из этого следует создать каждую отдельную вкладку «Виджет»)

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;
    }
}

Редактировать: я забыл опубликовать класс Command (хотя у вас наверняка есть свой собственный)

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;
    }
}

И, наконец, PropertyChangedBase (просто вспомогательный класс)

    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));
        }
    }

Результат:

введите здесь описание изображения

  • По сути, каждый тип элемента вкладки представляет собой Widget, который содержит свою собственную логику и данные.
  • Вы определяете всю логику и данные на уровне ViewModel или Model, а не на уровне пользовательского интерфейса.
  • Вы манипулируете данными, определенными на уровне ViewModel или Model, и обновляете пользовательский интерфейс через DataBinding, никогда не касаясь пользовательского интерфейса напрямую.
  • Обратите внимание, как я использую шаблоны данных для предоставления определенного пользовательского интерфейса. для каждого класса ViewModel элемента вкладки.
  • При копировании новой вкладки вы просто создаете новый экземпляр нужного ViewModel и добавляете его в ObservableCollection. DataBinding WPF автоматически обновляет пользовательский интерфейс на основе уведомления об изменении коллекции.
  • Если вы хотите создать дополнительные типы вкладок, просто создайте производные от MVVMTabItemViewModel и добавьте туда свою логику и данные. Затем вы создаете DataTemplate для этой новой ViewModel, а WPF позаботится обо всем остальном.
  • Вы никогда, никогда, никогда не манипулируете элементами пользовательского интерфейса в процедурном коде в WPF, если только для этого нет НАСТОЯЩЕЙ причины. Вы не «снимаете отметку» или «отключаете» элементы пользовательского интерфейса, потому что элементы пользовательского интерфейса ДОЛЖНЫ отражать СОСТОЯНИЕ данных, предоставляемых ViewModel. Таким образом, состояние «Отметить/снять отметку» или состояние «Включено/отключено» — это просто свойство bool в ViewModel, к которому привязывается пользовательский интерфейс.
  • Обратите внимание, как это полностью устраняет необходимость в ужасных хаках, подобных winforms, а также устраняет необходимость в VisualTreeHelper.ComplicateMyCode() подобных вещах.
  • Скопируйте и вставьте мой код в File -> New Project -> WPF Application и посмотрите на результаты сами.
person Federico Berasategui    schedule 09.09.2013
comment
Это круто, спасибо за все этому человеку! Кроме того, вы знаете, почему я должен получить StackOverflowException на InitializeComponent()? Там написано An unhandled exception of type 'System.StackOverflowException' occurred in mscorlib.dll. - person Eric after dark; 09.09.2013
comment
@EricAfterdark это происходит с моим кодом? ты что-нибудь изменил? Я думаю, что это заслуживает отдельного вопроса, не так ли? - person Federico Berasategui; 09.09.2013
comment
Это с вашим кодом. Я не верю, что что-то изменил, поэтому и спросил. Я думал, что это могло быть как, о, я забыл сделать это... что-то вроде этого. - person Eric after dark; 09.09.2013
comment
@Ericafterdark нет, в моем конце этого не происходит. Только что протестировано .. Убедитесь, что вы не кладете вещи внутрь себя, вызывая бесконечную последовательность содержащихся элементов пользовательского интерфейса. - person Federico Berasategui; 09.09.2013