Using RegisterViewWithRegion with derived usercontrol - wpf

I am writing an application with WPF using Prism and the Managed Extensibility Framework. The purpose is to allow developers to create there own 3rd party modules. To remove as much work as possible I have created some base classes in a Common module which already does most of the required work. (eg. drag n drop for views, adds MEF InheritedExportAttribute). One of these controls is called ModuleControl (derives from UserControl) shown below.
I have everything working great if my module has a view class directly derived from UserControl with one child ModuleControl in the XAML (see the examples below). This seems like a lot extra work for users. I'd like to make my view class derive from 'ModuleControl' just using code. If I do this as in the final example below, then RegisterViewWithRegion throws the following exception:
Activation error occured while trying to get instance of type ModuleView, key \"\"
System.Exception {Microsoft.Practices.ServiceLocation.ActivationException}
I realise this happens when the type can't be registered. So my questions are how can I achieve this? What am I doing wrong? Does RegisterViewWithRegion explicitly expect UserControl and nothing derived from it?
Here is an example of my first control located in Common.dll.
namespace Common.Controls
{
[InheritedExport]
public partial class ModuleControl: UserControl
{
private Point _anchor;
public ModuleControl()
{
InitializeComponent();
}
public ObservableCollection<IComponent> ModuleComponents
{
get
{
return (ObservableCollection<IComponent>)GetValue(ModuleComponentsProperty);
}
set
{
SetValue(ModuleComponentsProperty, value);
}
}
private void InitialiseCollection()
{
try
{
ModuleComponents = new ObservableCollection<IComponent>();
var components = ServiceLocator.Current.GetAllInstances(typeof(IComponent));
Assembly controlAssembly = UIHelper.FindAncestor<UserControl>(VisualTreeHelper.GetParent(this)).GetType().Assembly;
foreach (IComponent c in components)
{
if (c.ExportType.Assembly.Equals(controlAssembly))
{
ModuleComponents.Add(new Component(c.ViewType, c.Description));
}
}
}
catch (Exception e)
{
}
}
public string ModuleName
{
get
{
return (string)GetValue(ModuleNameProperty);
}
set
{
SetValue(ModuleNameProperty, value);
}
}
public static readonly DependencyProperty ModuleComponentsProperty =
DependencyProperty.Register("ModuleComponents", typeof(ObservableCollection<IComponent>), typeof(ModuleControl), new UIPropertyMetadata(null));
public static readonly DependencyProperty ModuleNameProperty =
DependencyProperty.Register("ModuleName", typeof(String), typeof(ModuleControl), new UIPropertyMetadata("No Module Name Defined"));
private void DragList_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// Store the mouse position
_anchor = e.GetPosition(null);
}
private void DragList_PreviewMouseMove(object sender, MouseEventArgs e)
{
// Get the current mouse position
Point mousePos = e.GetPosition(null);
Vector diff = _anchor - mousePos;
if (e.LeftButton == MouseButtonState.Pressed &&
Math.Abs(diff.X) > SystemParameters.MinimumHorizontalDragDistance ||
Math.Abs(diff.Y) > SystemParameters.MinimumVerticalDragDistance)
{
// Get the dragged ListViewItem
ListView listView = sender as ListView;
ListViewItem listViewItem = UIHelper.FindAncestor<ListViewItem>((DependencyObject)e.OriginalSource);
// Initialize the drag & drop operation
if (listViewItem != null)
{
DataObject dragData = new DataObject("moduleFormat", listViewItem.Content);
DragDrop.DoDragDrop(listViewItem, dragData, DragDropEffects.Move);
}
}
}
private void moduleControl_LayoutUpdated(object sender, EventArgs e)
{
if (ModuleComponents == null)
InitialiseCollection();
}
}
Following from the example A Prism 4 Application Checklist I built the following module modifying this to use MEF instead of Unity where appropriate.:
This module is located in RandomNumbers.dll
namespace RandomNumbers
{
[ModuleExport(typeof(RandomNumberModule))]
public class RandomNumberModule: IModule
{
public void Initialize()
{
var regionManager = ServiceLocator.Current.GetInstance<IRegionManager>();
regionManager.RegisterViewWithRegion("MyRegion", typeof(ModuleView));
}
}
}
The ModuleView XAML that works looks like this:
<UserControl x:Name="userControl" x:Class="RandomNumbers.Views.ModuleView"
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:common="clr-namespace:Common.Base;assembly=Common"
xmlns:commonctrls="clr-namespace:Common.Controls;assembly=Common"
mc:Ignorable="d">
<commonctrls:ModuleControl x:Name="moduleControl" ModuleName="Random Number Module" />
</UserControl>
The codebehind for this is:
namespace RandomNumbers.Views
{
[Export]
public partial class ModuleView : UserControl
{
private Point startPoint;
public ModuleView()
{
InitializeComponent();
}
}
}
As already mentioned, all the above code works perfectly. However if I replace the XAML and code behind with this, then I get the exception as described. I have tried leaving out the ExportAttribute but nothing changes.
namespace RandomNumbers.Views
{
[Export(typeof(UserControl))]
public class ModuleView : ModuleControl
{
public ModuleView() : base() { }
}
}

Related

OnPropertyChanged wont change when used wth observable collection and single property

Loads the dataGrid and populates the Datagrid a row of 1'
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
update();
//this.DataContext = this;
}
CricketEvent events = new CricketEvent();
private void update()
{
events.updateList(new CricketEvent[1] { new CricketEvent(){Runs="1"} });
DG1.ItemsSource = events.RunsList;
}
private void DG1_SelectedCellsChanged(object sender, SelectedCellsChangedEventArgs e)
{
Window1 windowToOpen = new Window1();
var selectedUser = this.DG1.SelectedItem;
windowToOpen.Show();
}
}
Main class that loads the OnPropertyChanged I have a List property and string property that calls the OnPropertyChanged but I want the individual "Runs" property to be updated on its own rather than the whole collection.
class CricketEvent : INotifyPropertyChanged
{
private ObservableCollection<CricketEvent> runsList;
public string runs { get; set; }
public CricketEvent(string numofRuns) {
this.Runs = numofRuns;
}
public CricketEvent() { }
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<CricketEvent> RunsList
{
get { return this.runsList; }
set
{
if (value != this.runsList)
{
this.runsList = value;
OnPropertyChanged("RunsList");
}
}
}
public string Runs
{
get { return runs; }
set
{
runs = value;
// Call OnPropertyChanged whenever the property is updated
OnPropertyChanged("Runs");
}
}
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
public ObservableCollection<CricketEvent> updateList(CricketEvent []events)
{
runsList = new ObservableCollection<CricketEvent>(events.ToList());
return runsList;
}
}
This is the update window that brings up a text box and should change the "1s" In the previous window to whatever is typed into the textbox
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
CricketEvent events = new CricketEvent();
MainWindow main = new MainWindow();
private void Button_Click(object sender, RoutedEventArgs e)
{
events.updateList(new CricketEvent[1] { new CricketEvent(txt1.Text.ToString()) });
main.DG1.ItemsSource = events.RunsList;
}
The Button_Click event in Window1 does not use the instance of MainWindow that is show - it creates a new Window instance (that is not shown) and adds the updated list to the DG1.ItemsSource property. To solve that, pass the original instance of Window to the created Window1 in constructor and use that.
However, you should review your update strategy (and code style) because there is potential for improvments:
It is not a good idea to create a new collection if you want to update just one property of one item. Observable collections provide change notification, so you dont have to recreate the collection at all.
Instead of assinging the collection in code behind, use databinding to bind the collection to the ItemsSource. DataBinding results in automatic update of GUI elements if the collection or one item of you collection changed.

Catel and setting usercontrol's DataContext

I've tried to find a solution myself but I was not able, for some reason the DataContext is not correctly set up in the usercontrol's viewmodel
The idea is to have a single usercontrol that permits to perform a query on a fixed collection and allows the user to drop a treeviewitem that holds an item of the collection (in case the user have the treeview open)
In my main view I've defined :
<views:PortfolioChooserView x:Name="PortfolioChooserView" DataContext="{Binding PortfolioCompleteBox}" Height="25" LoadDefaultValue="True" />
Where PortfolioCompleteBox is a ViewModel defined in the MainViewModel as
public PortfolioChooserViewModel PortfolioCompleteBox
{
get { return GetValue<PortfolioChooserViewModel>(PortfolioChooserViewModelProperty); }
set { SetValue(PortfolioChooserViewModelProperty, value); }
}
public static readonly PropertyData PortfolioChooserViewModelProperty = RegisterProperty("PortfolioCompleteBox", typeof(PortfolioChooserViewModel));
public MainViewModel(ICreditLimitRepository creditLimitRepository, IDynamicContainer dynamicContainer)
{
this.creditLimitRepository = creditLimitRepository;
this.dynamicContainer = dynamicContainer;
LoadCreditLimitsCommand = new Command<object>(OnLoadCreditLimitsExecute, (() => OnLoadCreditLimitsCanExecute));
var viewModelFactory = this.GetServiceLocator().ResolveType<IViewModelFactory>();
PortfolioCompleteBox = viewModelFactory.CreateViewModel<PortfolioChooserViewModel>(null);
Model = new FiltersLoadModel();
}
My problem is that on the PortFolioChooserView I've the DataContext set to null (and I got 2 calls to the PortFolioChooserViewModel, one from the MainViewModel and the other one from the PortFolioChooserView's viewmodel locator)
public partial class PortfolioChooserView
{
private PortfolioChooserViewModel viewModel;
readonly bool isFirstLoad = true;
/// <summary>
/// Initializes a new instance of the <see cref="PortfolioChooserView"/> class.
/// </summary>
///
public PortfolioChooserView()
{
InitializeComponent();
if (isFirstLoad)
{
PortfolioCompleteBox.AllowDrop = true;
DragDropManager.AddPreviewDragOverHandler(PortfolioCompleteBox, OnElementDragOver);
DragDropManager.AddDropHandler(PortfolioCompleteBox, OnElementDrop);
isFirstLoad = false;
this.Loaded += PortfolioChooserView_Loaded;
this.DataContextChanged += PortfolioChooserView_DataContextChanged;
}
}
void PortfolioChooserView_DataContextChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
{
int t = 0;
}
void PortfolioChooserView_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
viewModel = (PortfolioChooserViewModel)this.DataContext;
}
private void OnElementDragOver(object sender, Telerik.Windows.DragDrop.DragEventArgs e)
{
var options = Telerik.Windows.DragDrop.DragDropPayloadManager.GetDataFromObject(e.Data, TreeViewDragDropOptions.Key) as TreeViewDragDropOptions;
if (options != null)
{
var visual = options.DragVisual as TreeViewDragVisual;
if (visual != null) visual.IsDropPossible = true;
}
e.Handled = true;
}
private void OnElementDrop(object sender, Telerik.Windows.DragDrop.DragEventArgs e)
{
var context = ((IPortfolioAutoComplete)this.DataContext);
context.SetPortfolioAutoCompleteBox(e);
}
public static readonly DependencyProperty LoadDefaultValueProperty = DependencyProperty.Register(
"LoadDefaultValue", typeof(bool), typeof(PortfolioChooserView), new PropertyMetadata(default(bool)));
public bool LoadDefaultValue
{
get { return (bool)GetValue(LoadDefaultValueProperty); }
set { SetValue(LoadDefaultValueProperty, value); }
}
}
What am I doing wrong?
Thanks
Don't try to manage your own vm's
Catel will automatically accept a parent-vm as it's own vm as long as they are compatible. You don't need to handle this manually in your view loading in the view.
Instead of creating a VM in the parent VM, use a model only (so the vm only cares about what the VM itself should do). Then set the DC of the PortfolioChooserView to the model. Then the vm of the child view can accept the model in the ctor and be managed on it's own.
There are much better ways to communicate between vm's then trying to micro-manage like you are doing now. As always, see the docs.

ObservableCollection gets reset (item count 0) going between 2 views

I am very new to MVVM so hopefully this isnt a hard problem.
Basically I have two Views:-
1. Holds a Data Grid which displays everything inside the ObservableCollection object
2. The second view basically has two text boxes which adds to the ObservableCollection when the user presses OK on the form.
Bascially what I am doing is showing the 2nd view from the 1st view with a button click, button labelled "Add Project"
Then I enter the information I need to add to the ObservableCollection in the 2nd view. When I press OK on the form, It calls a method "AddMProduct" which basically adds a item to the collection inside the ViewModel.
But the problem is by doing this its creating a new ViewModel() object so therefore reinitialise the ObservableCollection. So therefore the collection gets reset back to zero.
So in the MVVM model, how do I basically retain the collection, between the 2 views and the ViewModel?
Thanks
------------------Code---------------
VIEW 1
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void btnAddProjectGroup_Click(object sender, RoutedEventArgs e)
{
// Open new window here to add a new project group
AddProductGroup dlg = new AddProductGroup();
dlg.ShowDialog();
}
}
VIEW 2
ProductGroupBindable newProduct = new ProductGroupBindable();
ProductsViewModel viewModel = null;
public AddProductGroup()
{
viewModel = new ProductsViewModel();
InitializeComponent();
}
private void btnOK_Click(object sender, RoutedEventArgs e)
{
newProduct.Id = System.Guid.NewGuid();
newProduct.Name = txtName.Text;
newProduct.Description = txtDescription.Text;
viewModel.AddProduct(newProduct);
this.Close();
}
VIEWMODEL
class ProductsViewModel : INotifyPropertyChanged
{
private ObservableCollection<ProductGroupBindable> m_ProductCollection;
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string name)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
public ObservableCollection<ProductGroupBindable> ProductCollection
{
get
{
Test();
return m_ProductCollection;
}
set
{
m_ProductCollection = value;
}
}
private void Test()
{
m_ProductCollection.Add(new ProductGroupBindable { Description = "etc", Name = "test12" });
}
public ProductsViewModel()
{
if (m_ProductCollection == null)
ProductCollection = new ObservableCollection<ProductGroupBindable>();
}
public void AddProduct(ProductGroupBindable newProduct)
{
ProductCollection.Add(newProduct);
NotifyPropertyChanged("ProductCollection");
}
Consider this simple option to fix the problem. Create an overload constructor of the dialog view that accepts ProductsViewModel as parameter. With that you can pass existing viewmodel object from MainWindow to the dialog, hence avoid instantiating new empty viewmodel :
//in MainWindow
private void btnAddProjectGroup_Click(object sender, RoutedEventArgs e)
{
//Open new window here to add a new project group
AddProductGroup dlg = new AddProductGroup(this.viewmodel);
dlg.ShowDialog();
}
//in AddProductGroup :
public AddProductGroup(ProductsViewModel vm)
{
viewModel = vm;
InitializeComponent();
}

change main form controls by trigger event inside canvas

I am currently working on a project that required me to use a canvas in order to draw rectangles around specific places in a picture (to mark places)
Each rectangle (actually "rectangle" since it is also a custom class that I created by inheriting from the Grid class and contain a rectangle object) contains properties and data about the marked place inside the picture.
my main form contains controls such as TextBox ,DropDownLists and etc.
Now what I am trying to do is that for each time I am clicking on the "rectangle" object the main form controls will be filled with the object data.
I do not have access to those controls from the canvas class.
this code is inside the costume canvas class to add the object into the canvas:
protected override void OnMouseLeftButtonDown( MouseButtonEventArgs e)
{
if(e.ClickCount==2)
{
testTi = new TiTest();
base.OnMouseLeftButtonDown(e);
startPoint = e.GetPosition(this);
testTi.MouseLeftButtonDown += testTi_MouseLeftButtonDown;
Canvas.SetLeft(testTi, e.GetPosition(this).X);
Canvas.SetTop(testTi, e.GetPosition(this).X);
this.Children.Add(testTi);
}
}
and by clicking an object that is placed inside the canvas i want to get the information.
for now just want to make sure i am getting the right object with a simple messagebox
void testTi_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
MessageBox.Show(sender.GetType().ToString());
}
this is my costume "Rectangle" class
class TiTest:Grid
{
private Label tiNameLabel;
private Rectangle tiRectangle;
private String SomeText = string.Empty;
private String version = "1.0";
private String application = "CRM";
private String CRID = "NNN";
public String SomeText1
{
get { return SomeText; }
set { SomeText = value; }
}
public Rectangle TiRectangle
{
get { return tiRectangle; }
set { tiRectangle = value; }
}
public Label TiNameLabel
{
get { return tiNameLabel; }
set { tiNameLabel = value; }
}
public TiTest()
{
this.SomeText = "Hello World!!";
this.TiNameLabel = new Label
{
Content = "Test Item",
VerticalAlignment = System.Windows.VerticalAlignment.Top,
HorizontalAlignment = System.Windows.HorizontalAlignment.Left
};
TiRectangle = new Rectangle
{
Stroke = Brushes.Red,
StrokeDashArray = new DoubleCollection() { 3 },//Brushes.LightBlue,
StrokeThickness = 2,
Cursor = Cursors.Hand,
Fill = new SolidColorBrush(Color.FromArgb(0, 0, 111, 0))
};
Background= Brushes.Aqua;
Opacity = 0.5;
this.Children.Add(this.tiNameLabel);
this.Children.Add(this.tiRectangle);
}
}
is there any way to access the main form controls from the costume canvas class or by the costume rectangle class?
Thanks in advance
You can have your main window be binded to a singletone ViewModel holding the properties of the rectangles.
ViewModel
public class MainWindowViewModel : INotifyPropertyChanged
{
#region Singletone
private static MainWindowViewModel _instance;
private MainWindowViewModel()
{
}
public static MainWindowViewModel Instance
{
get
{
if (_instance == null)
_instance = new MainWindowViewModel();
return _instance;
}
}
#endregion
#region Properties
private string _someInfo;
public string SomeInfo
{
get
{
return _someInfo;
}
set
{
if (_someInfo != value)
{
_someInfo = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("SomeInfo"));
}
}
}
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
In main window xaml
<TextBox Text="{Binding SomeInfo}"/>
Also set the view model as your main window data context (in main window constructor for exmaple)
this.DataContext = MainWindowViewModel.Instance;
Finally, from where you handle the click event of the rectangles (testTi_MouseLeftButtonDown), access the MainWindowViewModel instance and set it's properties accordingly.
MainWindowViewModel.Instance.SomeInfo = myRectangle.SomeInfo;
This will trigger the PropertyChanged event, which will update your control's on the main window.
If you are not familiar with the MVVM (Model, View. View Model) pattern you can read about it here
Hope this helps

How to use Prism within an ElementHost

I'm new to Prism and I am attempting to host a Prisim control within an ElementHost. I seem to be missing something very basic. I have a single WinForm that contains an ElementHost. The following code is in the form:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
Bootstrapper bootstrapper = new Bootstrapper();
bootstrapper.Run();
var child = bootstrapper.Container.Resolve<Shell>();
elementHost.Child = child;
}
The BootStrapper handles regisration
public class Bootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
Container.RegisterType<MyView>();
var shell = Container.Resolve<Shell>();
return shell;
}
protected override IModuleCatalog GetModuleCatalog()
{
ModuleCatalog catalog = new ModuleCatalog();
catalog.AddModule(typeof(MyModule));
return catalog;
}
}
The MyView.xaml is nothing more than a label at this point.
Shell.xaml is a UserControl that contains the following XAML:
<ItemsControl Name="MainRegion" cal:RegionManager.RegionName="MainRegion" />
The module code is minimal:
public class MyModule : IModule
{
private readonly IRegionViewRegistry _regionViewRegistry;
public MyModule(IRegionViewRegistry registry)
{
_regionViewRegistry = registry;
}
public void Initialize()
{
_regionViewRegistry.RegisterViewWithRegion("MainRegion", typeof(MyView));
}
}
I've been tracing deep into the Prism code trying to figure out why the View is never set into the region. Am I missing something basic?
The reason is this code in Prism:
private static bool RegionManager::IsInDesignMode(DependencyObject element)
{
// Due to a known issue in Cider, GetIsInDesignMode attached property value is not enough to know if it's in design mode.
return DesignerProperties.GetIsInDesignMode(element) || Application.Current == null
|| Application.Current.GetType() == typeof(Application);
}
The reason is that for the non-WPF application the Application.Current is NULL!
The solution:
Create an empty class that will inherit from System.Windows.Application. (Name doesn’t matter):
At the point of entry to a plug-in execute the following code:
public class MyApp : System.Windows.Application
{
}
if (System.Windows.Application.Current == null)
{
// create the Application object
new MyApp();
}
This is it – now you have an Application.Current that is not null and it’s not equal to typeof(Application).
#Mark Lindell Above worked for me. The only things I had to change are below.
My bootstrapper
public class Bootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
return this.Container.Resolve<Shell>();
}
protected override void InitializeShell()
{
base.InitializeShell();
if (System.Windows.Application.Current == null)
{
// create the Application object
new HelloWorld.Myapp();
}
//App.Current.MainWindow = (Window)this.Shell;
//App.Current.MainWindow.Show();
//MainWindow = (Window)this.Shell;
}
protected override void ConfigureModuleCatalog()
{
base.ConfigureModuleCatalog();
ModuleCatalog moduleCatalog = (ModuleCatalog)this.ModuleCatalog;
moduleCatalog.AddModule(typeof(HelloWorldModule.HelloWorldModule));
}
and my form class
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
//Create the ElementHost control for hosting the WPF UserControl
ElementHost host = new ElementHost();
host.Dock = DockStyle.Fill;
Bootstrapper bootstrapper = new Bootstrapper();
bootstrapper.Run(true);
//var uc = bootstrapper.Container.Resolve<Shell>(); This line threw error
//Create the WPF UserControl.
HelloWorld.Shell uc = new HelloWorld.Shell();
//Assign the WPF UserControl to the ElementHost control's Child property.
host.Child = uc;
//Add the ElementHost control to the form's collection of child controls.
this.Controls.Add(host);
}
}
}
And just to be clear, I added below class in the WPF PRISM application containing Shell.
public class MyApp : System.Windows.Application
{
}
Edit: Note that the Load method handler (of form) has to be created by
rightclicking form, In the properties window, go to events and double
clicl Load. Copying and pasting load event handler doesn't work.

Resources