1

Essentially, I have a markup issue. I have come up with a few solutions but I can't help but feel like this should be simpler. Rather than lead you down my convoluted path I thought I would share the simplest implementation and ask how you would address it.

MainPage.xaml

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="6" />
        <ColumnDefinition />
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="6" />
        <ColumnDefinition />
        <!--Additional Columns-->
    </Grid.ColumnDefinitions>
    <!--Row Definitions-->
    <Label Grid.Row="0" Grid.Column="0" Content="Vin:" HorizontalAlignment="Right" />
    <ctrl:CommandTextBox Grid.Row="0" Grid.Column="2" Command="{Binding CreateVehicleCommand}" CommandParameter="{Binding Text, RelativeSource={RelativeSource Self}}" />
    <Label Grid.Row="0" Grid.Column="3" Content="Manufacturer:" HorizontalAlignment="Right" />
    <TextBox Grid.Row="0" Grid.Column="5" IsEnabled="False" Text="{Binding Vehicle.Manufacturer, Mode=OneWay}" />
    <!--Additional Read Only Values-->
</Grid>

Given the example above, how can I get the Contents of the Grid into a View given the constraint that the Command to create the vehicle is outside of the DataContext to be created(Vehicle)?

If you do wish to look at my specific attempt, that question is here UserControl's DependencyProperty is null when UserControl has a DataContext

Community
  • 1
  • 1
Derrick Moeller
  • 4,808
  • 2
  • 22
  • 48
  • I don't see where you are struggling. Why can't you inject your model into your view model? Why would that break the TextBox? If changing how the ViewModel gets its data affects the View, you probably did it wrong. – BradleyDotNET Dec 16 '14 at 21:42
  • @BradleyDotNET You are correct, what I have shown works. But it seems incorrect? I don't believe I should be injecting my viewmodel's factory into a placeholder viewmodel that gets overwritten by the creation of the viewmodel by the factory. I'm beginning to wonder if I could extend UserControl to provide a second DataContext one for my viewmodel and one for the factory? – Derrick Moeller Dec 16 '14 at 21:48
  • Honestly? Your whole design seems ridiculously over complicated. Why do you even need a factory? Btw, I agree that the process you described seems terribly convoluted. Then again, so does your design. – BradleyDotNET Dec 16 '14 at 21:55
  • @BradleyDotNET I agree entirely, and am looking for an alternative design that delivers identical functionality. I have simplified my example and would appreciate the feedback. – Derrick Moeller Dec 17 '14 at 01:49
  • No problem. I might not have time to fully analyze and post until tonight/tomorrow morning, but I will look at it! – BradleyDotNET Dec 17 '14 at 01:55

2 Answers2

0

how can I get the Contents of the Grid into a View given the constraint that the Command to create the vehicle is outside of the DataContext to be created(Vehicle)?

This feels like a race condition more than an MVVM problem. I will address the issue first but make a secondary suggestion after.

  1. There are no reasons in which a ViewModel cannot contain another viewmodel as a reference and that reference is bound to using the INotifyPropertyChanged mechanisim.
  2. Or that your xaml (view) page contains a static reference to a ViewModel which the page (view) does not directly use in its DataContext, but that a certain control cannot bind to that static outside of the data context of the containing control.

Either way one can provide access (as also mentioned in the response to the other post you provided) by pointing to itself to get data or to provide an alternate plumbing which gets the data.


Or you can flatten your viewmodel to contain more information and handle this IMHO race condition so that this situation doesn't arise and the control as well as the grid can access information in a proper format.

I can't fully address the problem because you are more aware of the design goals and hazards which now must be worked around.

ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122
  • I'm not sure I understand your concerns about a race condition? I am aware that I have a number of options regarding the creation of a composite ViewModel. MainPageViewModel is a composite, I could likewise create a SubPageViewModel that contains the CreateVehicleCommand and the VehicleViewModel. I don't feel like this gets me very far though. What I am trying to do is create a View that can stand on it's own and display properties of a ViewModel. Contained within this view I would like the ability to create the ViewModel. – Derrick Moeller Dec 17 '14 at 16:56
  • @FrumRoll I see what you mean about the view creating items. Maybe the issue is trying to send a full VM to a control. What if you broke out the actual items of that newly created VM into individual dependency properties? Within the control it has on change notices for each property which calls a method which looks for a complete set of data and then 'turns on' the control. (I did similar in a Silverlight project awhile back) If I am still off tack; just disregard my thoughts. – ΩmegaMan Dec 17 '14 at 17:07
0

I've come up with something, I'm relatively happy with. This has saved me from creating 100s of composite ViewModel's and while it does introduce some unnecessary complexity it does dramatically reduce the amount copy/paste code I need to write.

VMFactoryViewModel.cs

public class CreatedViewModelEventArgs<T> : EventArgs where T : ViewModelBase
{
    public T ViewModel { get; private set; }

    public CreatedViewModelEventArgs(T viewModel)
    {
        ViewModel = viewModel;
    }
}

public class VMFactoryViewModel<T> : ViewModelBase where T : ViewModelBase
{
    private Func<string, T> _createViewModel;
    private RelayCommand<string> _createViewModelCommand;
    private readonly IDialogService _dialogService;

    /// <summary>
    /// Returns a command that creates the view model.
    /// </summary>
    public ICommand CreateViewModelCommand
    {
        get
        {
            if (_createViewModelCommand == null)
                _createViewModelCommand = new RelayCommand<string>(x => CreateViewModel(x));

            return _createViewModelCommand;
        }
    }

    public event EventHandler<CreatedViewModelEventArgs<T>> CreatedViewModel;

    private void OnCreatedViewModel(T viewModel)
    {
        var handler = CreatedViewModel;
        if (handler != null)
            handler(this, new CreatedViewModelEventArgs<T>(viewModel));
    }

    public VMFactoryViewModel(IDialogService dialogService, Func<string, T> createViewModel)
    {
        _dialogService = dialogService;
        _createViewModel = createViewModel;
    }

    private void CreateViewModel(string viewModelId)
    {
        try
        {
            OnCreatedViewModel(_createViewModel(viewModelId));
        }
        catch (Exception ex)
        {
            _dialogService.Show(ex.Message);
        }
    }
}

VMFactoryUserControl.cs

public class VMFactoryUserControl<T> : UserControl where T : ViewModelBase
{
    public static readonly DependencyProperty VMFactoryProperty = DependencyProperty.Register("VMFactory", typeof(VMFactoryViewModel<T>), typeof(VMFactoryUserControl<T>));

    public VMFactoryViewModel<T> VMFactory
    {
        get { return (VMFactoryViewModel<T>)GetValue(VMFactoryProperty); }
        set { SetValue(VMFactoryProperty, value); }
    }
}

GenericView.xaml

<ctrl:VMFactoryUserControl x:Class="GenericProject.View.GenericView"
                           x:TypeArguments="vm:GenericViewModel"
                           xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                           xmlns:ctrl="clr-namespace:SomeProject.Controls;assembly=SomeProject.Controls"
                           xmlns:vm="clr-namespace:GenericProject.ViewModel">
    <Grid>
        <!-- Column Definitions -->
        <!-- Row Definitions -->
        <Label Grid.Row="0" Grid.Column="0" Content="Generic Id:" HorizontalAlignment="Right" />
        <ctrl:CommandTextBox Grid.Row="0" Grid.Column="2"
                             Command="{Binding VMFactory.CreateViewModelCommand, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
                             CommandParameter="{Binding Text, RelativeSource={RelativeSource Self}}" />
        <Label Grid.Row="0" Grid.Column="3" Content="Generic Property:" HorizontalAlignment="Right" />
        <TextBox Grid.Row="0" Grid.Column="5" IsEnabled="False" Text="{Binding GenericProperty, Mode=OneWay}" />
        <!--Additional Read Only Values-->
    </Grid>
</ctrl:VMFactoryUserControl>

GenericView.xaml.cs

public partial class GenericView : VMFactoryUserControl<GenericViewModel>
{
    public GenericView()
    {
        InitializeComponent();
    }
}

MainPageViewModel.cs

public class MainPageViewModel : ViewModelBase
{
    private readonly IDialogService _dialogService;
    private GenericViewModel _generic;
    private readonly VMFactoryViewModel<GenericViewModel> _genericFactory;

    public GenericViewModel Generic
    {
        get { return _generic; }
        private set
        {
            if (_generic != value)
            {
                _generic = value;
                base.OnPropertyChanged("Generic");
            } 
        }
    }

    public VMFactoryViewModel<GenericViewModel> GenericFactory
    {
        get { return _genericFactory; }
    }

    private void OnGenericFactoryCreatedViewModel(object sender, CreatedViewModelEventArgs<GenericViewModel> e)
    {
        Generic = e.ViewModel;
    }

    public MainPageViewModel(IDialogService dialogService)
    {
        _dialogService = dialogService;

        _genericFactory = new VMFactoryViewModel<GenericViewModel>(_dialogService, x => new GenericViewModel(_dialogService, GetGeneric(x)));
        _genericFactory.CreatedViewModel += OnGenericFactoryCreatedViewModel;
    }

    private Generic GetGeneric(string genericId)
    {
        // Return some Generic model.
    }
}

MainPage.xaml

<Page x:Class="GenericProject.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:vw="clr-namespace:GenericProject.View">
    <StackPanel>
        <!-- Headers and Additional Content. -->
        <vw:EventView DataContext="{Binding Generic}"
                      VMFactory="{Binding DataContext.GenericFactory, RelativeSource={RelativeSource AncestorType={x:Type Page}}}" />
    </StackPanel>
</Page>
Derrick Moeller
  • 4,808
  • 2
  • 22
  • 48