load Prism modules from dll files (at startup) - c#

Atm in my application I do like this:
class Bootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
return Container.Resolve<Shell>();
}
protected override void InitializeShell()
{
base.InitializeShell();
App.Current.MainWindow = (Window)Shell;
App.Current.MainWindow.Show();
}
protected override void ConfigureModuleCatalog()
{
base.ConfigureModuleCatalog();
var moduleCatalog = (ModuleCatalog)ModuleCatalog;
moduleCatalog.AddModule(typeof(FooModule));
moduleCatalog.AddModule(typeof(BarModule));
}
}
I would like to load FooModule and BarModule by indicating the path to the dll file, something like this:
protected override void ConfigureModuleCatalog()
{
...
var assembly = Assembly.LoadFrom(#"libs\FooLib.dll");
var type = assembly.GetType("FooLib.FooModule");
moduleCatalog.AddModule(type);
...
}
but it doesn't work, I get this error message on Bootstrapper.Run() :
There is currently no moduleTypeLoader in the ModuleManager that can retrieve the specified module.
EDIT:
this is my FooModule:
public class FooModule : IModule
{
private readonly IRegionViewRegistry _regionViewRegistry;
public FooModule(IRegionViewRegistry registry)
{
_regionViewRegistry = registry;
}
public void Initialize()
{
_regionViewRegistry.RegisterViewWithRegion("MainRegion", typeof(Main));
}
}

Ok, try to make your ConfigureModuleCatalog looking like this:
protected override void ConfigureModuleCatalog()
{
string path = #"libs\FooLib.dll";
var assembly = Assembly.LoadFrom(path);
var type = assembly.GetType("FooLib.FooModule");
ModuleCatalog.AddModule(new ModuleInfo
{
ModuleName = type.Name,
ModuleType = type.AssemblyQualifiedName,
Ref = new Uri(path, UriKind.RelativeOrAbsolute).AbsoluteUri
});
}
The key thing is:
Ref = new Uri(path, UriKind.RelativeOrAbsolute).AbsoluteUri
prism checks whether Ref property refers to physical file(file://) and loads assembly from this file.

I think Prism v4 Loading modules on demand with DirectoryModuleCatalog could help.
UPDATE:
Sorry, just realized that reference mentioned above won't help.
Try this one from msdn - "Loading Modules on Demand" section, I think that's what you need.

One easier method not to enter path manually is, obtain it from type->assembly->location
Type module1Type = typeof(Module1.Module1);
string path = module1Type.Assembly.Location;
moduleCatalog.AddModule(
new ModuleInfo()
{
ModuleName = module1Type.Name,
ModuleType = module1Type.AssemblyQualifiedName,
Ref = new Uri(path, UriKind.RelativeOrAbsolute).AbsoluteUri
});
return moduleCatalog;

Related

Monogame Custom Importer/Processor not being detected by Content Manager

Pretty much what it says in the title.
I've built a second .dll file which contains my Content Importer/Processor and added that .dll file in my 'References' section for my Content Manager for my project, but nothing. The Manager doesn't detect my custom importer/processor. No idea what's going on and I wasn't able to find anyone else having an issue like this, so I was hoping someone more experienced could help me out here!
I am using Json.NET for the Json Deserialization by the way.
Thank you in advence :)
MapJson Code:
public class MapJson
{
[JsonProperty("name")]
public String Name = "";
[JsonProperty("width")]
public Int32 MapWidth = 0;
[JsonProperty("height")]
public Int32 MapHeight = 0;
}
Importer Code:
[ContentImporter(".amap", DefaultProcessor = "MapProcessor", DisplayName = "Map Importer - Engine")]
public class MapImporter : ContentImporter<MapJson>
{
public override MapJsonImport(string filename, ContentImporterContext context)
{
string json = new FileHandle(filename).ReadAll();
MapJson data = JsonConvert.DeserializeObject<MapJson>(json);
return data;
}
}
Processor Code:
[ContentProcessor(DisplayName = "Map Processor - Engine")]
public class MapProcessor : ContentProcessor<MapJson, MapJson>
{
public override MapJson Process(MapJson input, ContentProcessorContext context)
{
return input;
}
}
Writer Code:
[ContentTypeWriter]
public class MapWriter : ContentTypeWriter<MapJson>
{
protected override void Write(ContentWriter writer, MapJson value)
{
writer.Write(value.Name);
writer.Write(value.MapWidth);
writer.Write(value.MapHeight);
}
}
Reader Code:
public class MapReader : ContentTypeReader<Map>
{
protected override Map Read(ContentReader reader, Map existingInstance)
{
MapJson data = new MapJson();
data.Name = reader.ReadString();
data.MapWidth = reader.ReadInt32();
data.MapHeight = reader.ReadInt32();
// this constructor just sets my 'Map' class's Name, MapWidth and MapHeight variables
return new Map(data);
}
}
Well uhh this is embarrassing...
The solution was to first build the game project, then to actually re-build the content importer/processor project, and then to link it with the content manager!
I feel so stupid haha

Load a XAML file in a ResourceDictionary from a ClassLibrary

I'm trying to make a simple plugin system in which a plugins are dynamically loaded from .dll files on application's startup and show up in the UI.
This Answer seems to be exactly what I'm looking for. It uses MEF to load the plugins. I tried to create a simple project and follow the instructions. My solution has the following structure:
MeftTest (contains main MefTest.exe, references only MefTest.SDK and not plugins)
MefTest.SDK (contains the IPlugin.cs and IPluginViewModel.cs and the Engine.cs which loads the plugins from the application's directory)
MefTest.Plugin1 (contains first plugin, references MefTest.SDK)
MefTest.Plugin2 (contains second plugin, references MefTest.SDK)
MefTest.SDK -> IPlugin.cs
public interface IPlugin
{
IPluginViewModel ViewModel { get; }
ResourceDictionary View { get; }
string Title { get; }
}
MefTest.SDK -> Engine.cs
public class Engine
{
[ImportMany]
private IEnumerable<IPlugin> plugins { get; set; }
public async Task<ObservableCollection<IPlugin>> GetPlugins()
{
try
{
var folder = AppDomain.CurrentDomain.BaseDirectory;
var catalog = new AggregateCatalog();
//catalog.Catalogs.Add(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
catalog.Catalogs.Add(new DirectoryCatalog(folder));
var container = new CompositionContainer(catalog);
container.ComposeParts(this);
var result = new ObservableCollection<IPlugin>();
foreach (var p in plugins)
{
result.Add(p);
}
return result;
}
catch (Exception ex)
{
//I get the exception here...
var t = ex;
throw;
}
}
}
MefTest.Plugin1 -> Plugin1.cs
[Export(typeof(IPlugin))]
public class Plugin1 : IPlugin
{
private MainViewModel viewModel { get; set; }
public IPluginViewModel ViewModel
{
get { return viewModel; }
}
public ResourceDictionary View
{
get { return viewDictionary; }
}
private ResourceDictionary viewDictionary = new ResourceDictionary();
public string Title
{
get { return "Plugin 1"; }
}
[ImportingConstructor]
public Plugin1()
{
//I get the error here. tried both of these, none of them work
viewDictionary.Source =
// new Uri("pack://application:,,,/MefTest.Plugin1;component/views/main.xaml", UriKind.Absolute);
new Uri("/MefTest.Plugin1;component/views/main.xaml",
UriKind.Relative);
}
public override string ToString()
{
return Title;
}
}
however, I get the error Could not load file or assembly 'MefTest.Plugin1.dll, Culture=neutral' or one of its dependencies. The system cannot find the file specified.
The Main.xaml file is in MefTest.Plugin1\Views\Main.xaml folder. Output type of the project is ClassLibrary and Build Action of the xaml file is Page.
PS: I tried to reference the plugin directly and add it without the MEF (Plugins.Add(new Plugin3.Plugin3());) and it still threw the same exception. So I don't think the problem is with the MEF part of the solution.
How can I fix this? Also, is there a better option to this approach?
i do it this way in an xbap application...to retreive distant xaml. Try to do the same with your local resourtce
private ResourceDictionary LoadDictionary(string source)
{
Stream streamInfo = null;
ResourceDictionary dictionary = null;
try
{
streamInfo = DistantManager.Instance.GetResource(source);
if (streamInfo != null)
{
Uri baseUri = DistantManager.Instance.GetUri(source);
dictionary = XamlReader.Load(streamInfo) as ResourceDictionary;
dictionary.Source = baseUri;
}
}
catch (Exception e)
{
BusinessLogger.Manage(e);
return null;
}
return dictionary;
}
something like
Uri baseUri = new Uri(Mysource);
dictionary = XamlReader.Load(XXXX) as ResourceDictionary;
dictionary.Source = baseUri;
but on the other hand I do not understand why you want a ResourceDictionary as your plugin view...? just create the plugin user control ??

Plugin Application and WPF issues?

I am working on a project where allowing 3rd-party plugins is required. I have worked with plugins before and I never had a problem.
I'm sure my problem is because WPF doesn't like me using Assembly.LoadFile(file) & Activator.CreateInstance(t)!
The error I encounter is:
The component 'Servus.Forms.MainWindow' does not have a resource identified by the URI '/Servus;component/forms/mainwindow.xaml'.
which shows in my MainForm constructor at:
InitializeComponent();
If I load the plugins after loading the MainForm it loads without issues, however when opening any other forms(there are many in my application) I experience the same issue as about but with the relevant error for that particular form.
I have also tried to load the plugins in there own AppDomain like this:
PluginDomain temp = new PluginDomain();
PluginBase tempPlug = temp.GetPlugin(file);
With the following classes:
public class PluginDomain
{
public AppDomain CurrentDomain { get; set; }
public ServusAssemblyLoader CurrentAssemblyLoader { get; set; }
private readonly Random _rand = new Random();
public PluginDomain()
{
}
public PluginBase GetPlugin(string assemblyName)
{
try
{
string appBase = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var ads = new AppDomainSetup { ApplicationBase = appBase, PrivateBinPath = appBase, ShadowCopyFiles = "true" };
CurrentDomain = AppDomain.CreateDomain("ServusDomain_Plugin_" + _rand.Next(0, 100000), null, ads);
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
CurrentAssemblyLoader = (ServusAssemblyLoader)
CurrentDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(ServusAssemblyLoader).FullName);
AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve;
return CurrentAssemblyLoader.Load(assemblyName);
}
catch (Exception e)
{
CConsole.WriteLine("Error: " + e.Message);
}
finally
{
CurrentAssemblyLoader = null;
AppDomain.Unload(CurrentDomain);
}
return null;
}
static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
string[] parts = args.Name.Split(',');
string file = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "\\" + parts[0].Trim() + ".dll";
return Assembly.LoadFrom(file);
}
}
public class ServusAssemblyLoader : MarshalByRefObject, IAssemblyLoader
{
public PluginBase Load(string file)
{
Assembly asm = Assembly.LoadFrom(file);
foreach (Type t in asm.GetTypes())
{
if (t.IsSubclassOf(typeof(PluginBase)))
{
return (PluginBase)Activator.CreateInstance(t);
}
}
return null;
}
}
public interface IAssemblyLoader
{
PluginBase Load(string file);
}
This returns an TransparentProxy object like this:
{System.Runtime.Remoting.Proxies.__TransparentProxy}
However I am unsure how to use this as I was expecting it to return a PluginBase Object.
I have read that many people have also have this issue, they have answers that say to use a new AppDomain, but as you can see this doesn't help me right now.
I hope I have provided you enough information, can anyone help?
It turns out I had a few things wrong in my PluginDomain Class.
Fix #1:
Replace:
return (PluginBase)Activator.CreateInstance(t);
With:
(PluginBase)asm.CreateInstance(t.ToString());
Fix #2:
Remove:
AppDomain.Unload(CurrentDomain);
Fix #3: (Purely for debugging)
Replace:
return CurrentAssemblyLoader.Load(assemblyName);
With:
PluginBase obj = CurrentAssemblyLoader.Load(assemblyName);
return obj;
EDIT:
It should be noted that the new AppDomain wont be able to access objects in the old one; so my problem is only half fixed.

Reading and writing values in.NET .config files

I want to use a custom path for a user.config file, rather than have .NET read it from the default location.
I am opening the file like this:
ExeConfigurationFileMap configMap = new ExeConfigurationFileMap();
configMap.ExeConfigFilename = String.Format("{0}\\user.config",AppDataPath);
Configuration config = ConfigurationManager.OpenMappedExeConfiguration(configMap, ConfigurationUserLevel.PerUserRoamingAndLocal);
But I can't figure out how to actually read settings out of it, I get a compile error saying that the values are inaccessible when I try to get a value through AppData or ConfigurationSection.
Do I need to create some sort of a wrapper class to consume the data properly?
I was recently tasked with a similar problem, I had to change the location of where settings files were read from the default location in AppData to the Application directory. My solution was to create my own settings files that derived from ApplicationSettingsBase which specified a custom SettingsProvider. While the solution felt like overkill at first, I've found it to be more flexible and maintainable than I had anticipated.
Update:
Sample Settings File:
public class BaseSettings : ApplicationSettingsBase
{
protected BaseSettings(string settingsKey)
{ SettingsKey = settingsKey.ToLower(); }
public override void Upgrade()
{
if (!UpgradeRequired)
return;
base.Upgrade();
UpgradeRequired = false;
Save();
}
[SettingsProvider(typeof(MySettingsProvider)), UserScopedSetting]
[DefaultSettingValue("True")]
public bool UpgradeRequired
{
get { return (bool)this["UpgradeRequired"]; }
set { this["UpgradeRequired"] = value; }
}
}
Sample SettingsProvider:
public sealed class MySettingsProvider : SettingsProvider
{
public override string ApplicationName { get { return Application.ProductName; } set { } }
public override string Name { get { return "MySettingsProvider"; } }
public override void Initialize(string name, NameValueCollection col)
{ base.Initialize(ApplicationName, col); }
public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection propertyValues)
{
// Use an XmlWriter to write settings to file. Iterate PropertyValueCollection and use the SerializedValue member
}
public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection props)
{
// Read values from settings file into a PropertyValuesCollection and return it
}
static MySettingsProvider()
{
appSettingsPath_ = Path.Combine(new FileInfo(Application.ExecutablePath).DirectoryName, settingsFileName_);
settingsXml_ = new XmlDocument();
try { settingsXml_.Load(appSettingsPath_); }
catch (XmlException) { CreateXmlFile_(settingsXml_); } //Invalid settings file
catch (FileNotFoundException) { CreateXmlFile_(settingsXml_); } // Missing settings file
}
}
A few improvements:
1) Load it up a bit simpler, no need for the other lines:
var config = ConfigurationManager.OpenExeConfiguration(...);
2) Access AppSettings properly:
config.AppSettings.Settings[...]; // and other things under AppSettings
3) If you want a custom configuration section, use this tool: http://csd.codeplex.com/
I never ended up getting the Configuration Manager approach working. After spending a half day muddling with no progress, I decided to roll my own solution as my needs are basic.
Here is the solution I came up with in the end:
public class Settings
{
private XmlDocument _xmlDoc;
private XmlNode _settingsNode;
private string _path;
public Settings(string path)
{
_path = path;
LoadConfig(path);
}
private void LoadConfig(string path)
{
//TODO: add error handling
_xmlDoc = null;
_xmlDoc = new XmlDocument();
_xmlDoc.Load(path);
_settingsNode = _xmlDoc.SelectSingleNode("//appSettings");
}
//
//use the same structure as in .config appSettings sections
//
public string this[string s]
{
get
{
XmlNode n = _settingsNode.SelectSingleNode(String.Format("//add[#key='{0}']", s));
return n != null ? n.Attributes["value"].Value : null;
}
set
{
XmlNode n = _settingsNode.SelectSingleNode(String.Format("//add[#key='{0}']", s));
//create the node if it doesn't exist
if (n == null)
{
n=_xmlDoc.CreateElement("add");
_settingsNode.AppendChild(n);
XmlAttribute attr =_xmlDoc.CreateAttribute("key");
attr.Value = s;
n.Attributes.Append(attr);
attr = _xmlDoc.CreateAttribute("value");
n.Attributes.Append(attr);
}
n.Attributes["value"].Value = value;
_xmlDoc.Save(_path);
}
}
}

Issue recomposing on an ImportMany with MEF

I've got a property defined on a class that has the importManyAttribute defined for it, the declaration is as follows:
public const string FontStyleProvidersPropertyName = "FontStyleProviders";
[ImportMany(typeof(IFontStyleProvider), RequiredCreationPolicy = CreationPolicy.Shared, AllowRecomposition=true)]
public List<IFontStyleProvider> FontStyleProviders { get; set; }
on first run I build my composition container as follows
private CompositionContainer BuildCompositionContainer()
{
//build our composable parts catalog
Assembly executingAssembly = Assembly.GetExecutingAssembly();
CompositionContainer applicationContainer;
string localPath = Path.GetDirectoryName(executingAssembly.Location);
try
{
aggregateCatalog = new AggregateCatalog();
aggregateCatalog.Catalogs.Add(new AssemblyCatalog(executingAssembly));
if (!Directory.Exists(Path.Combine(localPath, ApplicationExtensionsPath)))
{
Directory.CreateDirectory(Path.Combine(localPath, ApplicationExtensionsPath));
}
exportsCatalog = new DirectoryCatalog(Path.Combine(localPath, ApplicationExtensionsPath));
aggregateCatalog.Catalogs.Add(new DirectoryCatalog(Path.Combine(localPath, ApplicationExtensionsPath)));
//create a composition container
return applicationContainer = new CompositionContainer(aggregateCatalog);
}
catch (Exception e)
{
Debug.Fail("Catalog Construction Failed", e.StackTrace);
throw;
}
}
At this point, everything works as expected, but I can't seem to trigger re-composition on "this" class instance. I have an import method as follows:
private void Import()
{
exportsCatalog.Refresh();
CompositionBatch batch = new CompositionBatch();
batch.AddPart(this);
applicationContainer.Compose(batch);
var copy = PropertyChanged;
if (copy != null)
{
copy(this, new PropertyChangedEventArgs(FontStyleProvidersPropertyName));
copy(this, new PropertyChangedEventArgs(MessageContainerViewModelsPropertyName));
}
}
It finds new types in the ApplicationExtensionPath folder used by the exportsCatalog just fine, but it never actually re-builds FontStyleProviders (or MessageContainerViewModels)
I've been through the documents a few times and I can't seem to figure out why.
The problem is you aren't actually adding the catalog which you call Refresh() on to the AggregateCatalog. Change this:
exportsCatalog = new DirectoryCatalog(Path.Combine(localPath, ApplicationExtensionsPath));
aggregateCatalog.Catalogs.Add(new DirectoryCatalog(Path.Combine(localPath, ApplicationExtensionsPath)));
To this:
exportsCatalog = new DirectoryCatalog(Path.Combine(localPath, ApplicationExtensionsPath));
aggregateCatalog.Catalogs.Add(exportsCatalog);
Also, once your class has been composed once you shouldn't compose it again. Just calling exportsCatalog.Refresh() will be enough to cause recomposition.

Categories