开发者

ViewModel Binding Error

开发者 https://www.devze.com 2023-02-03 11:02 出处:网络
I am experiencing an error. Here is the scenario: wpf app loads and the content of a ScrollViewer is bound to a property in a ViewModel called ActiveFunction (which has a type UserControl). A custom

I am experiencing an error. Here is the scenario:

  1. wpf app loads and the content of a ScrollViewer is bound to a property in a ViewModel called ActiveFunction (which has a type UserControl). A custom control (UserCtrl1) is set for that prope开发者_JAVA百科rty in the constructor of that ViewModel.
  2. A button is pressed which issues a command that sets the ActiveFunction property to a new UserControl (UserCtrl2). That is, this.ActiveFunction = new UserCtrl2();
  3. The new UserControl is loaded as the content of the ScrollViewer. Everything seems fine.
  4. Then, a button is pressed which issues a command that sets the ActiveFunction property back to the original UserControl (this.ActiveFunction = new UserCtrl1();).
  5. At this point, an exception is thrown - "Specified element is already the logical child of another element. Disconnect it first."

Can anyone help me resolve this issue. I'm happy to upload the whole VS solution if that helps (its not that big). I just really want to get an understanding of the pitfalls of this technology. I seem to be fighting against the technology right now, rather than harnessing its power.

Cheers


I made the most simple implementation of the scenario you described and it works without errors for me. I will post the code for it. Please point out where the differences are in your code.

<Window x:Class="LogicalChildException.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <DockPanel>
        <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal">
            <Button Name="ChangeUserControl" Click="ChangeUserControl_Click">Change UserControl</Button>
        </StackPanel>
        <ScrollViewer Content="{Binding ActiveFunction}">

        </ScrollViewer>
    </DockPanel>
</Window>

<UserControl x:Class="LogicalChildException.UserControl1"
             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" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <TextBlock Height="500" FontSize="30">
            UserControl One
        </TextBlock> 
    </Grid>
</UserControl>

<UserControl x:Class="LogicalChildException.UserControl2"
             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" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <TextBlock Height="300" FontSize="10">
            UserControl Two
        </TextBlock>

    </Grid>
</UserControl>

using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;

namespace LogicalChildException
{

    public partial class MainWindow : Window,INotifyPropertyChanged
    {
        public MainWindow()
        {
            InitializeComponent();
            ActiveFunction = new UserControl1();
            DataContext = this;
        }

        private void ChangeUserControl_Click(object sender, RoutedEventArgs e)
        {
            if (ActiveFunction is UserControl1)
                ActiveFunction = new UserControl2();
            else
                ActiveFunction = new UserControl1();
        }

        private UserControl _activeFunction;
        public UserControl ActiveFunction
        {
            get { return _activeFunction; }
            set
            {
                _activeFunction = value;
                if(PropertyChanged!=null)
                    PropertyChanged(this,new PropertyChangedEventArgs("ActiveFunction"));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}


I've investigated further and found that the problem is with a control which is contained in UserCtrl1. I'm completely lost with this. So, the easiest thing is for me to post my VS solution for the project online. It will be much simpler than me trying to explain something I don't understand (and easier for you too). Click here to download.

The exception can be recreated very easily. Run the solution (VS 2010), click the Progress button. Then click the Project button (which returns to the original user control that is loaded when the app first starts).

Note that UserCtrl1 is actually NewWork and UserCtrl2 is ProgressView.


The reason you are getting that error is that you are setting the content of the ScrollViewer to be a UserControl. By doing so you are setting the parent of UserCtrl1 (UserControl) to be the ScrollViewer. If you can't set two children to a ScrollViewer which is what is happening when you try to set UserCtrl2 as ActiveFunction. What you should really be doing is too leverage the power of ViewModels and DataTemplates within WPF.

From the code you have posted I changed it to use a more MVVM approach.

  1. Using ViewModels and DataTemplates. It is better to use viewmodels because it is pure code you don't need to mess around with this parent/child UI relationship anymore. You just set things as you would normal objects. By specifying the datatemplate for a certain class, you have the visual display done for you. This is the whole separation between code and visual aspects.
  2. Commands. I am using a Command to handle the button click. This is how you should be doing it too if you want to go down the MVVM route. On top of just help separating logic and view, you can also do unit tests against the commands too with the need of a UI.

Here is the MainWindow.

<Window x:Class="LogicalChildException.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:LogicalChildException"
    Title="MainWindow" Height="350" Width="525">
<Window.Resources>
    <ResourceDictionary>
        <DataTemplate DataType="{x:Type local:UserControl1ViewModel}">
            <local:UserControl1 />
        </DataTemplate>
        <DataTemplate DataType="{x:Type local:UserControl2ViewModel}">
            <local:UserControl2 />
        </DataTemplate>
    </ResourceDictionary>
</Window.Resources>
<DockPanel>
    <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal">
        <Button Command="{Binding SwitchCommand}">Change UserControl</Button>
    </StackPanel>
    <ScrollViewer Content="{Binding ActiveFunction}">

    </ScrollViewer>
</DockPanel>

Here is the code behind for the MainWindow.xaml.cs. Basically what I am doing here is I am setting the DataContext of this view to be the viewmodel. This is not the best way to do it because you are hardcoding things. Better way would be to leverage data templates and let WPF handle it.

    using System.Windows;

namespace LogicalChildException
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            DataContext = new MainViewModel();
        }
    }
}

Here is the code for the ViewModels. I have used the idea of a DelegateCommand which I found from here http://www.wpftutorial.net/DelegateCommand.html. UserControl1ViewModel and UserControl2ViewModel are just dummy objects but you could make them implement INotifyPropertyChanged and then use that for binding in your datatemplate.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Windows.Input;

namespace LogicalChildException
{
    public class DelegateCommand : ICommand
    {
        private readonly Predicate<object> _canExecute;
        private readonly Action<object> _execute;

        public event EventHandler CanExecuteChanged;

        public DelegateCommand(Action<object> execute)
            : this(execute, null)
        {
        }

        public DelegateCommand(Action<object> execute,
                       Predicate<object> canExecute)
        {
            _execute = execute;
            _canExecute = canExecute;
        }

        public bool CanExecute(object parameter)
        {
            if (_canExecute == null)
            {
                return true;
            }

            return _canExecute(parameter);
        }

        public void Execute(object parameter)
        {
            _execute(parameter);
        }

        public void RaiseCanExecuteChanged()
        {
            if (CanExecuteChanged != null)
            {
                CanExecuteChanged(this, EventArgs.Empty);
            }
        }
    }

    public class MainViewModel : INotifyPropertyChanged 
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private ICommand _switchCommand;
        private object _activeFunction;

        private object _userControl1;
        private object _userControl2;

        public MainViewModel()
        {
            _switchCommand = new DelegateCommand(OnSwitch);

            _userControl1 = new UserControl1ViewModel();
            _userControl2 = new UserControl2ViewModel();

            ActiveFunction = _userControl1;
        }

        public ICommand SwitchCommand
        {
            get
            {
                return _switchCommand;
            }
        }

        public object ActiveFunction
        {
            get
            {
                return _activeFunction;
            }
            set
            {
                if (_activeFunction != value)
                {
                    _activeFunction = value;
                    OnPropertyChanged("ActiveFunction");
                }
            }
        }            

        private void OnSwitch(object obj)
        {
            // do logic for switching "usercontrols" here
            if (ActiveFunction == null)
            {
                // if null, just set it to control 1
                ActiveFunction = _userControl1;
            }
            else
            {
                ActiveFunction = (ActiveFunction is UserControl1ViewModel) ? _userControl2 : _userControl1;
            }

        }

        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }

    public class UserControl1ViewModel
    {
    }

    public class UserControl2ViewModel
    {
    }
}

There are many areas here that you could improve upon to make cleaner in the MVVM world but this should help you resolve your issue that you are currently having.

0

精彩评论

暂无评论...
验证码 换一张
取 消