Laden...

UserControl mit ViewModel und Dependancy Properties erstellen

Erstellt von NOFX vor 7 Jahren Letzter Beitrag vor 7 Jahren 4.043 Views
N
NOFX Themenstarter:in
42 Beiträge seit 2015
vor 7 Jahren
UserControl mit ViewModel und Dependancy Properties erstellen

Hallo,

ich möchte zum darstellen von Daten ein UserControl erstellen, dass sauber nach MVVM aufgebaut ist und ein eigenes ViewModel besitzt, allerdings auch Dependancy Properties, damit ich bei der Einbindung im XAML Werte wie das Maximum etc. per Binding setzen kann.

Bislang sieht es so aus, dass ich im XAML.cs die Dependancy Properties erstelle:

        public int Maximum
        {
            get { return (int)GetValue(MaximumProperty); }
            set { SetValue(MaximumProperty, value); }
        }

        public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register("Maximum", typeof(int), typeof(Surface2D), new FrameworkPropertyMetadata(new PropertyChangedCallback(MaximumPropertyChanged)));


        private static void MaximumPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
             // do something
        }

und im ViewModel (von INotifyPropertyChanged abgeleitet) die restlichen Properties, für das View erstelle. Auf die Dependancy Properties kann ich im View dann auch zugreifen, wie z.B. so (der Name des View ist einfach userCOntrol):

<TextBlock Text="{Binding Maximum, ElementName=userControl}"/>

Meine Frage ist jetzt, wie ich das sauber hinbekomme ohne an zwei verschiedenen Stellen Daten halten und pflegen zu müssen. Gerade weil ich in Anhängigkeit einiger Dependancy Properties auch Werte für das ViewModel generieren möchte, ist das so ja arg unschön.

M
177 Beiträge seit 2009
vor 7 Jahren

Es ist nicht verkehrt, wenn dein UC mehrere DP hat.

Wenn dir das nicht gefällt kannst du auch eine Klasse erstellen in der alle notwendigen Informationen drinnen sind. Dann erstellst du eine DP namens DataSource oder so ähnlich und verwendest diese Klasse als Typ.

N
NOFX Themenstarter:in
42 Beiträge seit 2015
vor 7 Jahren

Mir geht es nicht primär um die Dependancy Properties; davon habe ich gerne mehrere, die optional übergeben werden.

Nur wo packe ich die DP am besten hin (geht ja wohl nur im xalm.cs??) und wie mache ich die die saubere Kommunikation mit dem ViewModel? Im xaml.cs komme ich ja über den DataContext an das ViewModel aber ist das sauber und komme ich irgendwie sauber an die Daten der DPs aus dem ViewModel?

D
985 Beiträge seit 2014
vor 7 Jahren

Wieso hat dein ViewModel eine DP?

Das hat da nichts verloren. Damit erstellt man Eigenschaften für ein Control/UIElement. Ein ViewModel benachrichtigt über die Implementierung von INotifyPropertyChanged.

Das sollte man nicht durcheinanderwerfen.

Gehen würde es zwar, macht aber wenig Sinn, denn bei einem ViewModel wird eine Änderung einer Eigenschaft kein Neuzeichnen des Viewmodels auslösen (was man per DP ja vermerken kann).

N
NOFX Themenstarter:in
42 Beiträge seit 2015
vor 7 Jahren

Mein ViewModel hat keine DPs, das möchte ich auch nicht. Ich registriere die DPs in der xaml.cs, möchte nur eben die Werte auch im ViewModel verwenden können.

Kann ich auf die Werte sauber zugreifen? Vom ViewModel aus ist ja die View nicht bekannt, worin die DPs registriert wurden.

Als Workaround habe ich das so gelöst, dass ich im ViewModel eine gleichlautende Property zu jeder DP im View erstellt habe und über den PropertyChangedCallback die Werte im VM aktualisiere. Das ist aber nicht wirklich sauber:

        public double Maximum
        {
            get { return (double)GetValue(MaximumProperty); }
            set { SetValue(MaximumProperty, value); }
        }

        public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register("Maximum", typeof(double), typeof(Surface2D));//, new FrameworkPropertyMetadata(new PropertyChangedCallback(OnPropertyChanged)));

        private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            ((Surface2DViewModel)((Surface2D)sender).DataContext).GetType().GetProperty(args.Property.Name).SetValue(((Surface2DViewModel)((Surface2D)sender).DataContext), ((Surface2D)sender).GetValue(args.Property));
        }
2.079 Beiträge seit 2012
vor 7 Jahren

Setz doch einfach ein TwoWay-Binding?
Dann wird jeder Wert der DP automatisch ins ViewModel übertragen

Abgesehen davon habe ich bisher kaum sinnvolle Anwendungsfälle von DependencyProperties gesehen, die sich nicht einfacher mit einem ViewModel lösen ließen.
Einzige Ausnahmen sind Controls, die so allgemein gehalten sind, dass ein ViewModel sich nicht lohnt. Ein NumericUpDown zum Beispiel, dafür würde ich kein ViewModel schreiben, sondern DPs erstellen.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

N
NOFX Themenstarter:in
42 Beiträge seit 2015
vor 7 Jahren

Ok, ich habe das jetzt auf ein View mit CodeBehind begrenzt und kein ViewModel mehr. Dadurch habe ich das Problem mit dem Updaten des ViewModels aus dem CodeBehind beim Setzen der DPs nicht mehr.

Allerdings kriege ich bei dem Binding im MainWindow das Problem, dass das UserControl seinen eigenen DataContext besitzt und ich gerade auf dem Schlauch stehe, wie ich beim Binding auf das ViewModel des MainWindows zugreife. Hier der XAML Code:

<Surface:Surface2D Maximum="{Binding MaximumMVW, BindsDirectlyToSource=True, FallbackValue=30}"/>

MaximumMVW ist die Maximum Property im MainViewModel, es wird aber der DataContext des UserCOntrols verwendet, indem diese Property natürlich nicht definiert ist.

709 Beiträge seit 2008
vor 7 Jahren

Du könntest beim Binding das MainControl als RelativeSource angeben und dort dann die entsprechende Eigenschaft des DataContexts (den musst du dann explizit beim Binding mit angeben) binden.

Alternativ kannst du auch einen BindingProxy (wie z.B. hier beschrieben: http://stackoverflow.com/a/22074985/1120056) nutzen.

N
NOFX Themenstarter:in
42 Beiträge seit 2015
vor 7 Jahren

Danke! Das hat perfekt funktioniert mit der RelativeSource:

<Surface:Surface2D Maximum="{Binding DataContext.MaximumMVW, BindsDirectlyToSource=True, FallbackValue=30, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ViewModel:MainWindow}}}"/>
2.079 Beiträge seit 2012
vor 7 Jahren

Was ich persönlich lieber mag, ist ein BindingProxy

Das funktioniert dann auch, wenn das Control mit dem Binding nicht im VisualTree liegt, beim ContextMenu ist das glaube der Fall.

Sähe dann so aus:

<ViewModel:MainWindow.Resources>
    <utils:BindingProxy x:Key="myBindingProxy" Data="{Binding}" />
</ViewModel:MainWindow.Resources>

<Surface:Surface2D Maximum="{Binding Data.MaximumMVW, Source={StaticResource myBindingProxy}}"/>

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

D
985 Beiträge seit 2014
vor 7 Jahren

Hier mal ein Beispiel-UserControl mit DPs am Control und einem ViewModel für das Control:


namespace WpfApp1
{
    public class MyControlViewModel : BindableObject
    {

        #region Property Value
        private int _value;

        public int Value
        {
            get { return _value; }
            set
            {
                if ( _value != value )
                {
                    _value = value;
                    OnPropertyChanged( );
                }
            }
        }

        #endregion

    }
}


namespace WpfApp1
{
    /// <summary>
    /// Interaktionslogik für MyControl.xaml
    /// </summary>
    public partial class MyControl : UserControl
    {
        public MyControl()
        {
            InitializeComponent( );
        }

        public int Maximum
        {
            get { return (int) GetValue( MaximumProperty ); }
            set { SetValue( MaximumProperty, value ); }
        }

        // Using a DependencyProperty as the backing store for Maximum.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty MaximumProperty =
            DependencyProperty.Register( "Maximum", typeof( int ), typeof( MyControl ), new PropertyMetadata( 30 ) );

        public MyControlViewModel ItemSource
        {
            get { return (MyControlViewModel) GetValue( ItemSourceProperty ); }
            set { SetValue( ItemSourceProperty, value ); }
        }

        // Using a DependencyProperty as the backing store for ItemSource.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ItemSourceProperty =
            DependencyProperty.Register( "ItemSource", typeof( MyControlViewModel ), typeof( MyControl ), new PropertyMetadata( null ) );

    }
}


<UserControl x:Class="WpfApp1.MyControl"
             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" 
             xmlns:local="clr-namespace:WpfApp1"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300" Height="Auto" Width="Auto">

    <Grid>

        <Slider 
            DataContext="{Binding ItemSource, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MyControl}}}"
            Maximum="{Binding Maximum, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MyControl}}}" 
            Value="{Binding Path=Value}"/>

    </Grid>
</UserControl>


namespace WpfApp1
{
    public class MainWindowViewModel : BindableObject
    {
        public MainWindowViewModel()
        {
            Foo = new MyControlViewModel { Value = 20, };
            Maximum = 100;
        }

        #region Property Foo
        private MyControlViewModel _foo;

        public MyControlViewModel Foo
        {
            get { return _foo; }
            set
            {
                if ( _foo != value )
                {
                    _foo = value;
                    OnPropertyChanged( );
                }
            }
        }
        #endregion

        #region Property Maximum
        private int _maximum;

        public int Maximum
        {
            get { return _maximum; }
            set
            {
                if ( _maximum != value )
                {
                    _maximum = value;
                    OnPropertyChanged( );
                }
            }
        }
        #endregion

    }
}


<Window x:Class="WpfApp1.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <Grid>
        <local:MyControl ItemSource="{Binding Path=Foo}" Maximum="{Binding Path=Maximum}"/>
    </Grid>
</Window>

und schon stehst du dir nicht mehr auf den Füßen herum 😁