3

I've got the need for a class that identifies what type of network join to be performed.

The network join can either be a Domain join a Workgroup join.

In the case of a Workgroup join I only need to know the Name of the workgroup to join.

In the case of a Domain join I need to know the Name of the domain to join as well as the Username and Password to use (let's ignore all concerns about security here, this is a scenario).

I then want to create a WPF UI for it similar to this:

http://documents.weber.edu/ctctools/sccm/images/osdnotes/9.png

Where the credentials portion of the GUI becomes disabled if the user picks a Workgroup join and enabled when they pick a Domain join (with the same being said regarding the name of the actual Workgroup/Domain to join).

And I want to be able to serialize/deserialize this data (again ignore security concerns, this is a scenario).

The way I see it I have two options:

Option 1

Create a solution similar to:

enum JoinType
{
    Domain,
    Workgroup
}

class NetworkJoin
{
    JoinType JoinType {get; set;}
    string Name {get;set;}
    string Username {get;set;}
    SecureString Password {get;set;}

    void Join()
    {
        // Join code for domain + workgroup
    }
}

This would allow me to easily do TextBoxUsername.IsEnabled = ViewModel.NetworkJoin.JoinType == JoinType.Domain.

However, because the class instance is serialized/deserialized it allows for an instance of this class to have JoinType = JoinType.Workgroup as well as having a Username/Password and it's an assumption (although a logical one) that what network join to do is based off of a check on the JoinType (rather than say, if (Username == null) { // workgroup join })

Which brings me to option 2

Option 2

Something similar to:

interface INetworkJoin
{
    string Name {get;set;}
    void Join();
}

class DomainJoin : INetworkJoin
{
    string Name {get;set;}
    string Username {get;set;}
    SecureString {get;set;}
    void Join()
    {
        // Domain join code
    }
}

class WorkgroupJoin : INetworkJoin
{
    string Name {get;set;}
    void Join()
    {
        // Workgroup join code
    }
}

Now it's impossible for you to create an object with the wrong properties or any assumption about what type of join will be performed because ambiguous parameters we passed.

In fact, normally this would be a much better solution. Except for the binding it to the UI.

My ViewModel would basically have a INetworkJoin NetworkJoin, which means my View would only see a INetworkJoin. It needs to know its concrete type in order to determine whether to show/not show the credentials objects, it needs to bind the Username TextBox to the Username property (which INetworkJoin doesn't have...), same for Password, etc etc.

Summary

If feels like neither of the solutions is really appropriate. The first provides an ambiguous join and the second would require finding out the concrete type of an interface, as well as accessing properties only available in the concrete type.

I imagine there has to be a better solution than either of these, but this is really all I can think of.

Any help would be appreciated.

cogumel0
  • 2,430
  • 5
  • 29
  • 45
  • You can combine your both options by adding JoinType to interface (only getter). Class implementation of this field would look like this: public string JoinType JoinType { get => JoinType.Domain; } // I dont really know if this is "appropriate", but i do it all the time :) – Mailosz Nov 28 '18 at 16:46
  • When I come across these kinds of things, I don't want to check for exact type, but I do need to know which of the types _could_ be appropriate. There's an option of a Strategy pattern, where you pass a parameter indicating "HasCredentials", and the provider impl will give you the correct type. Helpful: https://en.wikipedia.org/wiki/Strategy_pattern – Dan Rayson Nov 28 '18 at 17:07

4 Answers4

3

You should have separate domain models and view models. View models also usually implement INotifyPropertyChanged and have additional properties e.g. to enable/disable buttons etc.

Domain Model:

abstract class NetworkJoin
{
    public string Name { get; set; }
}

class WorkgroupJoin : NetworkJoin
{
}

class DomainJoin : NetworkJoin
{
    public string Username { get; set; }
    public SecureString Password { get; set; }
}

View model (for sake of simplicity I am not showing the INotifyPropertyChanged implementation. Changes to Name, Username and Password would have to trigger OnPropertyChanged(nameof(IsOkButtonEnabled))):

class NetworkJoinViewModel
{
    private const int MinPasswordLength = 8;

    public JoinType JoinType { get; set; }
    public string Name { get; set; }
    public string Username { get; set; }
    public SecureString Password { get; set; }

    public bool IsOkButtonEnabled
    {
        get {
            switch (JoinType) {
                case JoinType.Domain:
                    return
                        !String.IsNullOrEmpty(Name) &&
                        !String.IsNullOrEmpty(Username) &&
                        Password != null && Password.Length >= MinPasswordLength;
                case JoinType.Workgroup:
                    return !String.IsNullOrEmpty(Name);
                default:
                    return false;
            }
        }
    }

    public bool IsLoginEnabled => JoinType == JoinType.Domain; // For password an username textboxes.

    public void Join()
    {
        switch (JoinType) { /* ... */  }
    }

    public NetworkJoin ToDomainModel() {
        switch (JoinType) {
            case JoinType.Domain:
                return new DomainJoin {
                    Name = Name,
                    Username = Username,
                    Password = Password
                };
            case JoinType.Workgroup:
                return new WorkgroupJoin {
                    Name = Name
                };
            default:
                return null;
        }
    }
}

Finally a view model factory (because I do not want to add a ToViewModel method to the domain model. The domain model should not know details about the view models):

static class NetworkJoinViewModelFactory
{
    public static NetworkJoinViewModel Create(NetworkJoin networkJoin)
    {
        switch (networkJoin) {
            case WorkgroupJoin workgroupJoin:
                return new NetworkJoinViewModel {
                    JoinType = JoinType.Workgroup,
                    Name = workgroupJoin.Name
                };
            case DomainJoin domainJoin:
                return new NetworkJoinViewModel {
                    JoinType = JoinType.Domain,
                    Name = domainJoin.Name,
                    Username = domainJoin.Username,
                    Password = domainJoin.Password
                };
            default:
                return null;
        }
    }
}
Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
  • Is there a reason you chose an abstract class without any implementation over an interface? (off topic, I know) – Dan Rayson Nov 28 '18 at 17:02
  • An abstract can implement common logic. But this is not a statement against an interface. You can also have an abstract class implement such an interface and then program against this interface. – Olivier Jacot-Descombes Nov 28 '18 at 17:08
  • I think "abstract class" implies that it contains _some_ logic. To me, this base class is defining how parts of the application communicate with each other in a common way, which is interface, in my mind. Alas, it matters not. :) – Dan Rayson Nov 28 '18 at 17:11
  • 1
    An interface also gives you the possibility to start with a new implementation from scratch, if you don't like the implementation of a given abstract base class. Often you start by defining an interface and later, when you write the implementations, you decide to use an abstract base class because it allows you to reuse things. But as you said, it does not really matter. – Olivier Jacot-Descombes Nov 28 '18 at 17:19
  • Who orchestrates the creation of a WorkgroupJoinViewModel or DomainJoinViewModel based on the JoinType? With this design I need some sort of "master" ViewModel to do the orchestration, unless I'm missing something. – Alexander Pope Nov 28 '18 at 18:30
  • You start by creating either a `WorkgroupJoin` or `DomainJoin` domain model. Then you call the factory: `NetworkJoin nj = new WorkgroupJoin(); NetworkJoinViewModelBase vm = NetworkJoinViewModelFactory.Create(nj);`. I.e. There is no more an `enum JoinType`. The join type is defined by the type of the domain or view model class itself. – Olivier Jacot-Descombes Nov 28 '18 at 18:46
  • 1
    How does that help? Say I start with a WorkgroupJoin and want to switch to a DomainJoin. I need a "MasterVM" to handle the event and create a new DomainJoin view model. This doesn't even touch the factory because I have yet to hit the Domain Model layer. The way I see it, that factory is only useful when you want to hydrate your view models using some deserialised domain models from your persistence layer. – Alexander Pope Nov 28 '18 at 18:55
  • You are right. Switching the join type in the UI is a problem. Therefore I changed the implementation to use a single view model class again, now containing the join type as enum. – Olivier Jacot-Descombes Nov 28 '18 at 19:30
  • If you start by creating a domain model object, why even have the factory at all? The `NetworkJoinViewModel` is aware of both domain model classes *and* you must start by creating the actual domain model, so why not just pass a domain model object to the ctor of the `NetworkJoinViewModel`? – cogumel0 Nov 28 '18 at 20:58
  • I like your idea but I think that there's no need for a `ToDomainModel` (in fact I think it complicates it). I think a simpler approach would be that the ctor of the VM accepts a domain model and keeps one private field for each domain model type. You cannot realistically have more than one unique domain model instance, but rather than "hard" making it a singleton just store one instance of each on the VM. – cogumel0 Nov 28 '18 at 21:13
  • The other thing is that to allow for a separate `TextBox` for the domain name and for the workgroup name I think that you need to have two properties: DomainName and WorkgroupName. That would allow you to swap between join types without losing the old value from the `TextBox`, but then I wonder if it really wouldn't be easier to have two VMs, one per domain join type and just a "master VM" with a `ActiveNetworkJoinViewModel` property. – cogumel0 Nov 28 '18 at 21:15
  • My first Idea was to formulate the VM as a wrapper around the domain model;however, there is the join type switching problem. It would require you to switch between domain models and to pass over the common fields. It is easier to create the final domain model at the end with `ToDomainModel`. As for the other way round (DM -> VM), yes you could incorporate the factory stuff in the VM's constructor. – Olivier Jacot-Descombes Nov 29 '18 at 14:18
0

I think that as a general principle to change the code of your logic to make it easier for your GUI is a code smell.

In general your viewModel will have to do some processing to data to make it suitable for displaying. If that includes dynamically checking it's type, that's perfectly reasonable - it's a fairly common thing to do.

You can throw an exception if an instance of INetWorkJoin is not one of your two types.

In functional programming there's the concept of a Union type, which is perfect for this sort of situation. There are ways to simulate them in C# using internal constructors in an abstract class. However it may not be worth the effort of doing this.

For example

public abstract class NetworkJoin
{
    internal NetworkJoin(){}
    string Name {get;set;}
    void Join();
}

public class DomainJoin : NetworkJoin
{
    public DomainJoin() : base(){}
    string Name {get;set;}
    string Username {get;set;}
    SecureString {get;set;}
    void Join()
    {
        // Domain join code
    }
}

public class WorkgroupJoin : NetworkJoin
{
    public WorkGroupJoin : base(){}
    string Name {get;set;}
    void Join()
    {
        // Workgroup join code
    }
 }

The advantage of this is you can guarantee that noone can implement your base class outside the assembly, so there will only ever be two implementations of the abstract class.

Yair Halberstadt
  • 5,733
  • 28
  • 60
0

I suggest using composition instead of inheritance.

Bind the DomainJoinVM to a DomainJoinView and the WorkgroupJoinVM to a WorkgroupJoinView. Unite both views in the NetworkJoinView.

                           NetworkJoinView
                           +      +      +
                          /       |       \
            DomainJoinView        |        WorkgroupJoinView
                 +                |                +
                 |                |                |
                 |                |                |
           DomainJoinVM           |         WorkgroupJoinVM
           +             \        |       /               +
           |              +       |      +                | 
IJoinService               NetworkJoinVM                  IJoinService

I also suggest declaring an IJoinService interface to encapsulate and abstract away the Domain Model layer. You then inject it into the view models. This decouples the Domain Model from your View Model, and lets you implement the persistence(serialization) as needed.

Think of it as divide et impera for UI development.

public class NetworkJoinVM
{
    public DomainJoinVM DomainJoin; 
    public WorkgroupJoinVM WorkgroupJoin;
    public JoinType JoinWith;

    public NetworkJoinVM(DomainJoinVm domainJoin, WorkgroupJoinVm workgroupJoin)
    {
        DomainJoin = domainJoin;
        WorkgroupJoin = workgroupJoin;
    }

    public void Join()
    {
        if(JoinWith == JoinType.Domain)
            DomainJoin.Join();
        else
            WorkgroupJoin.Join();
    }
}


public class DomainJoinVM
{
    private IJoinService _joinService; 

    public string DomainName;
    public string UserName;
    public string Password;

    public DomainJoinVM(IJoinService joinService)
    {
        _joinService = joinService;
    }

    public void Join()
    {
        _joinService.DomainJoin(DomainName, UserName, Password);
    }
}


public class WorkgroupJoinVM
{
    private IJoinService _joinService;

    public string Name;

    public WorkgroupJoinVM(IJoinService joinService)
    {
        _joinService = joinService;
    }

    public void Join()
    {
        _joinService.WorkgroupJoin(Name);
    }
}


public enum JoinType
{
    Domain,
    Workgroup
}

You can register the interface implementations in the composition root of your application, and hide the Model details from your ViewModels. This works best using a DI container, but is easy to implement "manually" as well.

public interface IJoinService
{
    void WorkgroupJoin(string workgroup);
    void DomainJoin(string domain, string username, string password);
}
Alexander Pope
  • 1,134
  • 1
  • 12
  • 22
  • I'm assuming this does not make use of the interface/classes from Option 2, so I'm assuming the serialized object would be the `NetworkJoinVM`, in which case that is not ideal since I would be serializing an instance of each concrete join type, something I don't want. – cogumel0 Nov 28 '18 at 21:41
  • If you serialize NetworkJoinVM and the user selected Workgroup, you can nullify DomainJoin. Depending on serialization output/framework you can ignore null fields/props. Still, I advise against serializing any ViewModel. Instead you can create a DTO that contains all the data relevant to that screen and serialize it. I recommend reading https://stackoverflow.com/questions/725348/plain-old-clr-object-vs-data-transfer-object and Fowler's article https://martinfowler.com/eaaCatalog/dataTransferObject.html – Alexander Pope Nov 28 '18 at 22:13
  • Then, on deserialization, you can use a factory to hydrate the ViewModel, as seen in Olivier Jacot-Descombe's answer and my comments to him. – Alexander Pope Nov 28 '18 at 22:18
  • But then why not do something like this: https://pastebin.com/xKY2Y1x7 (theory only, haven't testd it). It seems simpler in implementation and I can just get the object to serialize by calling `VM.ActiveJoin`. Of course that's missing INPC code, which would actually be slightly more complex with this implementation but unless I'm missing something and it doesn't work it feels very simply and robust. – cogumel0 Nov 28 '18 at 22:34
  • And of course it's also missing the code for changing the `ActiveJoin` which would come from a method that would literally just switch to one not currently active and be invoked on the radio button check changed event. – cogumel0 Nov 28 '18 at 22:36
  • Because you break encapsulation. DomainJoinVM should handle events and INPC code from DomainJoinView (think of password validation for example). In your pastebin it looks simple because currently you have few fields and not much logic, but you loose all the benefits of having separate VMs for Workgroup and Domain, and you are left with a complicated variant of your Option 1. – Alexander Pope Nov 28 '18 at 22:52
  • You need to decide between purity of design(my example) and ease of implementation(Option 1 in OP). This screen looks simple, and you can get away with 1, in more complex screens you can apply the above. If reusability is a concern it can be easily mixed in with WPF user controls. – Alexander Pope Nov 28 '18 at 23:01
  • What encapsulation though? The `DomainJoin` and `NetworkJoin` are not VMs in that example (and INetworkJoin should not implement INotifyPropertyChanged), they are just POCOs to all intents an purposes. They are not aware of the existence of a VM (`NetworkJoinViewModel`) or a View or anything else for that matter. – cogumel0 Nov 28 '18 at 23:05
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/184437/discussion-between-alexander-pope-and-cogumel0). – Alexander Pope Nov 28 '18 at 23:12
0

In this case, I would not bind the UI to the model directly. I would have the ViewModel have its own fields for the data.

The big benefit of this is that, if the user changes their selection from "Domain" to "Workgroup" and then back to "Domain", you haven't lost the domain username and password that they entered. But you still get to keep the integrity of your domain-model objects (you never end up with a WorkgroupJoin that has a username in it).

For the data model, I'd follow your Option 2. The viewmodel could take an INetworkJoin in its constructor, and scrape the data from it; and then later, when you're ready to save, you would call a method on the viewmodel (shown here as GetModel) to create a new INetworkJoin with the user-entered data.

public class NetworkJoinViewModel : INotifyPropertyChanged
{
    private JoinType _joinType;
    private string _name;
    private string _domainUserName;
    private string _domainPassword;

    public NetworkJoinViewModel(INetworkJoin join) {
        _name = join.Name;
        if (join is DomainJoin domainJoin) {
            _joinType = JoinType.Domain;
            _domainUserName = domainJoin.Username;
            _domainPassword = domainJoin.Password;
        } else if (join is WorkgroupJoin workgroupJoin) {
            _joinType = JoinType.Workgroup;
            _domainUserName = "";
            _domainPassword = "";
        } else throw new ArgumentException("Unknown INetworkJoin implementation");
    }

    public JoinType JoinType {
        get { return _joinType; }
        set {
            _joinType = value;
            NotifyPropertyChanged(nameof(JoinType));
            NotifyPropertyChanged(nameof(IsUserNameEnabled));
            NotifyPropertyChanged(nameof(IsPasswordEnabled));
        }
    }
    public bool IsUserNameEnabled => _joinType == JoinType.Domain;
    public bool IsPasswordEnabled => _joinType == JoinType.Domain;
    // ... typical INPC implementations of Name, UserName, and Password

    public INetworkJoin GetModel() {
        if (_joinType == JoinType.Domain) {
            return new DomainJoin(Name, DomainUserName, DomainPassword);
        } else if (_joinType == JoinType.Workgroup) {
            return new WorkgroupJoin(Name);
        } else throw new InvalidOperationException("Unknown JoinType");
    }
}
Joe White
  • 94,807
  • 60
  • 220
  • 330
  • Like your approach, but shouldn't the private fields be properties so that you can bind the controls to them? Also you say `The big benefit of this is that, if the user changes their selection from "Domain" to "Workgroup" and then back to "Domain", you haven't lost the domain username and password that they entered` - which is true, but you're still losing the Workgroup/Domain name when switching between one or the other. – cogumel0 Nov 28 '18 at 21:35
  • This is why I'm thinking the best approach might be to have a VM with one instance of each concrete `INetworkJoin` and to then have a property `INetworkJoin ActiveNetworkJoin` and then go `public string Name { get => ActiveNetworkJoin.Name; set => ActiveNetworkJoin.Name = value;}` and similar things for the properties `Username` and `Password` (but with type checking and returning `null` if the `ActiveNetworkJoin` is of type `WorkgroupJoin`. – cogumel0 Nov 28 '18 at 21:38
  • That also means I don't need to have a `JoinType`. I have my `ActiveNetworkJoin` that holds all of that for me. – cogumel0 Nov 28 '18 at 21:41
  • @cogumel0 Yes, there should be properties. That's why I have the `// ... typical INPC implementations of Name, UserName, and Password` comment. I didn't bother showing code for the properties, but they should have the usual getter and setter-with-notification. – Joe White Nov 28 '18 at 21:42
  • And no, you don't lose the workgroup/domain name when switching. That's just the Name property; it doesn't change when you switch JoinType. (Unless you *want* the user to have to enter a new name after changing type. If you *do* want them to have to enter a new name, you can just make two properties on the viewmodel, one for DomainName and one for WorkgroupName.) – Joe White Nov 28 '18 at 21:46
  • True, you don't *lose it* but if I start with Workgroup and Name set to `MyWorkgroup` when I go to Domain now the Domain TextBox says `Workgroup`. If I replace that with `MyDomain.Local` and go back to Workgroup now the Workgroup TextBox says `MyDomain.Local` (assuming separate Name TextBoxes of course - as per the picture) – cogumel0 Nov 28 '18 at 21:56
  • Guess my point is, I find this example (missing INPC implementation, code for swapping `ActiveJoin` and INetworkJoin doesn't need to implement `INotifyPropertyChanged`) easier than yours: https://pastebin.com/xKY2Y1x7 and doesn't suffer from the problem of attempting to bind a single property (Name) to two controls (one TextBox for Domain another for Workgroup). Feedback on that code would be appreciated. – cogumel0 Nov 29 '18 at 08:54