Add a Property to Entity Class in ViewModel

2019-08-31 16:37发布

问题:

I have a profile with a EntityCollection of ProfileUser. In this Class I have a Profile_ID ald a Profile relation and a User_ID but no USer relation, because User is in another Database.

In a Datagrid I want to access via User.Username

I tried this, but ofc it doesnt work...

public EntityCollection<ProfileUser> ProfileUsers
    {
        get
        {
            if (profile != null) return profile.ProfileUser;
            else return null;
        }
        set
        {
            profile.ProfileUser = value;
        }
    }

and here my custom Class

public class ProfileUserExtended : ProfileUser
{
    public Operator User
    {
        get
        {
            return OperatorManager.GetByGuId(this.User_ID);
        }
    }
}

Of course I cannot construct the derived class by the base class. But I need this Operator be part of the collection I bind to...

I hope you understand my problem and can help.

edit: This Converter solved the Problem for me:

public class OperatorConverter:IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        try
        {
            if (!(value is Guid)) return null;
            if (!(parameter is string)) return null;

            var guid = (Guid)value;
            var par = (string)parameter;

            var op = OperatorManager.GetByGuId(guid);
            if (op == null) return null;

            var prop = op.GetType().GetProperty(par);
            if (prop == null) return null;

            return prop.GetValue(op, null);
        }
        catch (Exception e)
        {
            throw (e);
        }
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new System.NotImplementedException();

    }
}

In XAML:

<DataGridTextColumn Header="Name" Binding="{Binding Path=User_ID,Converter={StaticResource ResourceKey=operatorConverter},ConverterParameter='Name'}" IsReadOnly="True" />

回答1:

Here are three ways to achieve several bindings similar to what you need to do.

1) a wrapper class:

public class ProfileUserWrapper : DependencyObject
{
    public ProfileUserWrapper(ProfileUser thebrain) { TheUser = thebrain; }

    public ProfileUser TheUser { get; private set; }

    public Operator User { get { if (_user != null)return _user; return _user = OperatorManager.GetByGuId(TheUser.User_ID); } }
    private Operator _user = null;
}

Now, instead of having a public EntityCollection<ProfileUser> ProfileUsers you may expose for example IEnumerable<ProfileUserWrapper>:

public EntityCollection<ProfileUser> ProfileUsers // your original code
{
    get{ if (profile != null) return profile.ProfileUser; else return null;}
    set { profile.ProfileUser = value; }
}

public IEnumerable<ProfileUserWrapper> ProfileUsers2
{
    get { return ProfileUsers.Select(user => new ProfileUserWrapper(user));
}

then bind to the ProfileUsers2, and some of your bindings should be changed from "Address" to "TheUser.Address", but this almost surely will work.

2) second, a smart-converter, for example:

public class OperatorPicker : IValueConverter
{
    public object Convert(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        var pu = value as ProfileUser;
        if (pu != null)
            return OperatorManager.GetByGuId(pu.User_ID);
        return null;
    }

    public object ConvertBack(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new System.NotImplementedException();
    }
}

It almost couldn't be simplier to write. Now you can use the converter in the XAML binding:

<Window.Resources>
    <myextra:OperatorPicker x:Key="conv1" />
</Window.Resources>

<Grid>
    <ListBox x:Name="lbxFirst" ItemsSource="{Binding ProfileUsers}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Margin="5" Text="{Binding User_ID}" />
                    <TextBlock Margin="5" Text="{Binding Login}" />
                    <TextBlock Margin="5" Text="{Binding Address}" />
                    <TextBlock Margin="5" Text="{Binding Path=., Converter={StaticResource conv1}}" />
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>

But this way, you will get the Operator object it self. It will be hyper-hard to allow you to bind to the properties of operator returned in such way, as the Binding already has path fixed at ".", and you cannot change that, as the Converter must be passed a ProfileUser instance..

3) third, most complex but completely working without any wrappers is based on an attached property, converter and a two bindings, but you could do it also on two attached properties and one change callback. I prefer the former way, so here it is:

public class DummyClass : DependencyObject
{
    public static readonly DependencyProperty TheOperatorProperty = DependencyProperty.RegisterAttached(
        "TheOperator", typeof(Operator), typeof(DummyClass),
        new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender)
    );

    public static Operator GetTheOperator(DependencyObject elem) { return (Operator)elem.GetValue(TheOperatorProperty); }
    public static void SetTheOperator(DependencyObject elem, Operator value) { elem.SetValue(TheOperatorProperty, value); }
}

    ... xmlns:myextra="clr-namespace:...." ....

<Window.Resources>
    <myextra:OperatorPicker x:Key="conv1" />
</Window.Resources>

<Grid>
    <ListBox x:Name="lbxFirst" ItemsSource="{Binding ProfileUsers}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel x:Name="aparent" Orientation="Horizontal"
                            myextra:DummyClass.TheOperator="{Binding Path=., Converter={StaticResource conv1}}">
                    <TextBlock Margin="5" Text="{Binding User_ID}" />
                    <TextBlock Margin="5" Text="{Binding Login}" />
                    <TextBlock Margin="5" Text="{Binding Address}" />
                    <TextBlock Margin="5"
                               Text="{Binding Path=(myextra:DummyClass.TheOperator).OperatorCodename, ElementName=aparent}" />
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>

Please note that there is a new binding at the level of StackPanel. This one can be placed anywhere in the data template, even on the textbox itself. Note how it translates a ProfileUser into an Operator via a converter. The Path=. is not required, but I added it so the exact meaning of the binding is shown. Note how the last textbox binding is specified: it is a binding to that attached property (via element-name), not to the original data!

This time I've tested everything and it works on my side with no DependencyObject inheritance on the ProfileUser. it is satisfied by the DummyClass inheritance. If you try this, please drop me a note as usual :)



回答2:

EDIT: this is quite a good solution, but it turned out that it requires that the target data object inherits from DependencyObject which is not the case. Here, the data object probably inherits from EntityObject and this cannot be changed, as it is the source DAO object:)

Having said that, lets play with Attached Properties:


You sometimes can solve it quite easily with Attached Properties. The binding engine can not only bind to normal properties or dependency properties. Actually it can bind to 5 or more things, and the easiest one of them are the Att.Props.

An attached property is a "virtual" or rather a "fake" property (lets name it "IsZonk") defined in some random class (call it ABC) , but registered in a special way so that the binding engine treats it as if it was present on your specified target class (let's say XYZ). Any attempt to access IsZonk on XYZ via bindings will cause the binding engine to bounce the request to the ABC class. Moreover, the resulting method call in the ABC class will be given the exact XYZ object that the request originated from.

This way you can easily extend existing objects with new data or even new functionalities without even modifying them. It is very like "static extension methods" added to C# in the 3.5 version.

namespace Whatever {

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

        lbxFirst.DataContext = new MyModel();
    }
}

public class MyModel
{
    public IEnumerable<ProfileUser> ProfileUsers
    {
        get
        {
            var tmp = new[]
            {
                new ProfileUser{ User_ID = "001", Login = "Adelle", Address = "123 Shiny Street" },
                new ProfileUser{ User_ID = "002", Login = "Beatrice", Address = "456 Sleepy Hill" },
                new ProfileUser{ User_ID = "003", Login = "Celine", Address = "789 Rover Dome" },
            };

            tmp[0].SetValue(ProfileUserExtras.UserProperty, new Operator { RelatedUser = tmp[0], OperatorCodename = "Birdie", PermissionLevel = 111 });
            tmp[1].SetValue(ProfileUserExtras.UserProperty, new Operator { RelatedUser = tmp[1], OperatorCodename = "Twin", PermissionLevel = 222 });
            tmp[2].SetValue(ProfileUserExtras.UserProperty, new Operator { RelatedUser = tmp[2], OperatorCodename = "Trident", PermissionLevel = 333 });

            return tmp;
        }
    }
}

public class ProfileUser : DependencyObject
{
    public string User_ID { get; set; }
    public string Login { get; set; }
    public string Address { get; set; }
    //- Operator User {get{}} -- does NOT exist here
}

public class Operator
{
    public ProfileUser RelatedUser { get; set; }
    public string OperatorCodename { get; set; }
    public int PermissionLevel { get; set; }
}

public static class ProfileUserExtras
{
    public static readonly DependencyProperty UserProperty = DependencyProperty.RegisterAttached(
        "User",             // the name of the property
        typeof(Operator),   // property type
        typeof(ProfileUserExtras), // the TARGET type that will have the property attached to it

        new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender) // whatever meta you like
    );
}

}

The last thing is to use it in XAML:

.... xmlns:myextra="clr-namespace:Whatever" ....

<ListBox x:Name="lbxFirst" ItemsSource="{Binding ProfileUsers}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock Margin="5" Text="{Binding User_ID}" />
                <TextBlock Margin="5" Text="{Binding Login}" />
                <TextBlock Margin="5" Text="{Binding Address}" />
                <TextBlock Margin="5" Text="{Binding Path=(myextra:ProfileUserExtras.User).OperatorCodename}" />
                <TextBlock Margin="5" Text="{Binding Path=(myextra:ProfileUserExtras.User).PermissionLevel}" />
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Please pay attention to the notation of an attached property in the binding. It must be enclosed in parenthesis and also almost always will need a namespace prefix, elsewise the binding engine will look for something else and somewhere else. By adding the parens you indicate that this property is an 'attached property' instead of normal one.