Unity C# – Accessing subclass properties from variable defined as derived class - c#

I'm trying to write an xml parser to take some data in a game and build out objects for me. Right now I want to go through the nodes and build out different config objects based on the node/attributes.
foreach (XmlNode node in actionList) {
ActionConfig config;
if (some checks determine action is "Fire") {
config = new FireActionConfig();
config.speed = (float)node.Attributes["speed"].Value;
}
// do something with config
}
The error I get is "ActionConfig does not contain a definition for speed...". I tried casting config as FireActionConfig even though it's already defined as one.

foreach (XmlNode node in actionList) {
ActionConfig config;
if (some checks determine action is "Fire") {
FireActionConfig fireConfig = new FireActionConfig();
fireConfig.speed = Single.Parse( node.Attributes["speed"].Value );
config = fireConfig;
}
// do something with config
}

Related

How to apply an attribute to all classes who inherit monobehaviour in a project/ an assembly?

I'm asking this because I want to eliminate the "Failed to insert item" warnings in a Unity project, which is caused by amount of scripts exceeding a certain value. Here is a solution provided by unity, which suggests applying [AddComponentMenu("")]attribute to all monobehaviours accordingly.
Since I'm working on a project with lots of scripts, it seems very difficult to apply this attribute manually. I need to:
Finding all classes under \Assets\Scripts (or in Assembly-CSharp) who inherit monobehaviour or any of its child class
Applying [AddComponentMenu("PATH")] attibute to the classes found above and put the realtive path of the script in the "PATH"
I don't know how to implement these and have been failing to find a solution by myself.
Some additional features would be wonderful (not necessary):
If there is existing [AddComponentMenu("")] in the script, do not override the original one.
Automatically applying [AddComponentMenu("")] to the scripts to be created.
Looking forward to all kinds of help. Thanks a lot!
There are probably many ways to do this outside of Unity. However, I will assume you want to do it all in Unity itself just for the simpleness of things.
Remark: There are for sure edge-cases left but I hope this is a good starting point
Finding all classes under \Assets\Scripts (or in Assembly-CSharp) who inherit monobehaviour or any of its child class
This first point would be quite simple - kind of. The Assets don't allow to find script types themselves but you can get all script assets by searching for t:MonoScript in the search bar in the Assets view.
The same can be done also via script using AssetDatabase.FindAssets like e.g.
var scriptGUIDs = AssetDataBase.FindAssets($"t:{nameof(MonoScript)}", new[] {"Assets/Scripts"});
From there you will need to load the script asset and check its type using e.g.
foreach (var scriptGUID in scriptGUIDs)
{
// e.g. Assets/Scripts/MyComponent.cs
var scriptAssetPath = AssetDatabase.GUIDToAssetPath(scriptGUID);
// first get the actual asset
var scriptAsset = AssetDatabase.LoadAssetAtPath<MonoScript>(scriptAssetPath);
// get the system type
var scriptType = scriptAsset.GetClass();
// Now first of all check if this is actually something derived from MonoBehaviour -> if not skip
if(!scriptType.IsSubclassOf(typeof(MonoBehaviour))) continue;
.....
}
Applying [AddComponentMenu("PATH")] attribute to the classes found above and put the realtive path of the script in the "PATH"
This is of course way more tricky since it requires to find the according code line (right above the class implementation) and add text into the file.
So this requires multiple steps
Check if this type already has the attribute -> skip, which also solves
If there is existing [AddComponentMenu("")] in the script, do not override the original one.
Load the raw text content
find the line of code which contains public class TYPENAME :
Insert your attribute line before that line
Write all lines back to the file
After all done refresh the data base to cause a reload/recompilation
This could look somewhat like e.g.
// Find all assets of type "MonoScript" in the folder Assets/Scripts
// or remove /Scripts if you really want to go for all in the Assets
var scriptGUIDs = AssetDatabase.FindAssets($"t:{nameof(MonoScript)}", new[] {"Assets/Scripts"});
foreach (var scriptGUID in scriptGUIDs)
{
// e.g. Assets/Scripts/MyComponent.cs
var scriptAssetPath = AssetDatabase.GUIDToAssetPath(scriptGUID);
// first get the actual asset
var scriptAsset = AssetDatabase.LoadAssetAtPath<MonoScript>(scriptAssetPath);
// get the system type
var scriptType = scriptAsset.GetClass();
// Now first of all check if this is actually something derived from MonoBehaviour -> if not skip
if(!scriptType.IsSubclassOf(typeof(MonoBehaviour))) continue;
// check if this class already has the attribute -> if so skip
if(scriptType.IsDefined(typeof(AddComponentMenu), true)) continue;
// otherwise load in the lines as a list so we can insert our new line
var lines = File.ReadAllLines(scriptAssetPath).ToList();
// find the one defining the class
var indexOfClassLine = lines.FindIndex(l => l.Contains($" class {scriptType.Name} : "));
// e.g. Assets/Scripts/MyComponent
// => you might want to e.g. also remove the "Assets" part ;)
var relativePathWithoutExtension = scriptAssetPath.Substring(0, scriptAssetPath.Length - 3);
// insert the new attribute one line right above that line
lines.Insert(indexOfClassLine, $"[AddComponentMenu(\"{relativePathWithoutExtension}\")]");
File.WriteAllLines(scriptAssetPath, lines);
}
AssetDatabase.Refresh();
And finally
Automatically applying [AddComponentMenu("")] to the scripts to be created.
Somewhere in you project you need an implementation of AssetModificationProcessor which will basically do the same thing as before, just this time it is slightly easier since we don't need to check the type since all c# scrips created via the menu are MonoBehaviour by default
public class ScriptKeywordProcessor : UnityEditor.AssetModificationProcessor
{
public static void OnWillCreateAsset(string path)
{
// ignore meta files
var tempPath = path.Replace(".meta", "");
var index = tempPath.LastIndexOf(".");
if (index < 0) return;
// ignore if not a .cs script
var file = tempPath.Substring(index);
if (file != ".cs") return;
// otherwise load in the lines as a list so we can insert our new line
var lines = File.ReadAllLines(path).ToList();
// find the one defining the class
var indexOfClassLine = lines.FindIndex(l => l.Contains($" class {Path.GetFileNameWithoutExtension(path)} : "));
// e.g. Assets/Scripts/MyComponent
// => you might want to e.g. also remove the "Assets" part ;)
var relativePathWithoutExtension = path.Substring(0, path.Length - 3);
// insert the new attribute one line right above that line
lines.Insert(indexOfClassLine, $"[AddComponentMenu(\"{relativePathWithoutExtension}\")]");
File.WriteAllLines(path, lines);
AssetDatabase.Refresh();
}
}

Check if an xml section exist in a file using XDocument

I have some code that reads in an xml file. However it is triggering an error at the 3rd IF statement:
if (xdoc.Root.Descendants("HOST").Descendants("Default")
.FirstOrDefault().Descendants("HostID")
.FirstOrDefault().Descendants("Deployment").Any())
Error:
System.NullReferenceException: Object reference not set to an instance of an object.
That is because in this particular file there is no [HOST] section.
I was assuming that on the first IF statement, if it didn't find any [HOST]section it would not go into the statement and therefore i should not get this error. Is there a way to check if a section exists first?
XDocument xdoc = XDocument.Load(myXmlFile);
if (xdoc.Root.Descendants("HOST").Any())
{
if (xdoc.Root.Descendants("HOST").Descendants("Default").Any())
{
if (xdoc.Root.Descendants("HOST").Descendants("Default").FirstOrDefault().Descendants("HostID").FirstOrDefault().Descendants("Deployment").Any())
{
if (xdoc.Root.Descendants("HOST").Descendants("Default").FirstOrDefault().Descendants("HostID").Any())
{
var hopsTempplateDeployment = xdoc.Root.Descendants("HOST").Descendants("Default").FirstOrDefault().Descendants("HostID").FirstOrDefault().Descendants("Deployment").FirstOrDefault();
deploymentKind = hopsTempplateDeployment.Attribute("DeploymentKind");
host = hopsTempplateDeployment.Attribute("HostName");
}
}
}
}
Within the body of this if block...
if (xdoc.Root.Descendants("HOST").Descendants("Default").Any())
{
if (xdoc.Root.Descendants("HOST").Descendants("Default").FirstOrDefault().Descendants("HostID").FirstOrDefault().Descendants("Deployment").Any())
{
if (xdoc.Root.Descendants("HOST").Descendants("Default").FirstOrDefault().Descendants("HostID").Any())
{
var hopsTempplateDeployment = xdoc.Root.Descendants("HOST").Descendants("Default").FirstOrDefault().Descendants("HostID").FirstOrDefault().Descendants("Deployment").FirstOrDefault();
deploymentKind = hopsTempplateDeployment.Attribute("DeploymentKind");
host = hopsTempplateDeployment.Attribute("HostName");
}
}
}
...you have established that the element <Root>/HOST/Default exists. You however don't know whether <Root>/HOST/Default/HostId/Deployment exists. If it doesn't you will get a NullReferenceException like the one you're experiencing due to the use of FirstOrDefault. It is generally recommended to use First in cases where you expect the elements to be present, which will give you at least a better error message.
If you expect the elements to be not present, a simple solution is to use the ?. along the respective LINQ2XML axis:
var hopsTemplateDeployment =
xdoc.Root.Descendants("HOST").Descendants("Default").FirstOrDefault()
?.Descendants("HostID").FirstOrDefault()
?.Descendants("Deployment").FirstOrDefault();
if (hopsTemplateDeployment != null)
{
deploymentKind = hopsTemplateDeployment.Attribute("DeploymentKind");
host = hopsTemplateDeployment.Attribute("HostName");
}
It will also save you the chain of nested if clauses.

Creating instances of classes in a particular folder

I have a certain folder with a couple of view classes (XAML files).
Right now i am instantiating these by code:
engineRoomView = new EngineRoomView()
{
DataContext = new ProcessViewModel()
};
and then further down:
item = new TabItem();
item.Contents = engineRoomView;
item.Name = "Engine Room";
views.Add(item);
What I want to achieve is some kind of dynamic code for creating one instance of each view in that particular folder without knowing about them during programming.
If a developer adds another xaml file to that folder. Then this gets created in run-time.
Something imaginary like:
Foreach(file in folder)
{
magicInstance = createInstanceFromFile(file);
MainViewModel.addView(magicInstance);
}
Is this possible?
If I understand you correctly, this could be archived with the build in Xaml Reader. The Xaml Reader can read a xaml file and will generate the objects based on the xaml.
Have a look here:
Loading XAML at runtime?
It sounds like you have a "Parent View" that you want to automatically attach a child view for each file in the same folder.
If the classes in each folder have a namespace consistent with the folder structure, this code should allow you to create a list of an instances of each class in the same folder as an example instance that inherit from a base class (could modify easily for interface also).
static class NamespaceHelper
{
public static List<Type> FindTypesInSameNamespaceAs(object instance)
{
string ns = instance.GetType().Namespace;
Type instanceType = instance.GetType();
List<Type> results = instance.GetType().Assembly.GetTypes().Where(tt => tt.Namespace == ns &&
tt != instanceType).ToList();
return results;
}
public static List<T> InstantiateTypesInSameNamespaceAs<T>(object instance)
{
List<T> instances = new List<T>();
foreach (Type t in FindTypesInSameNamespaceAs(instance))
{
if (t.IsSubclassOf(typeof(T)))
{
T i =(T) Activator.CreateInstance(t);
instances.Add(i);
}
}
return instances;
}
}
Just call NamespaceHelper.InstantiateTypesInSameNamespaceAs<YourBaseViewType>(instanceOfParentViewInSameFolder), loop through the results, and add them to your Parent.
Foreach(ViewBase v in NamespaceHelper.InstantiateTypesInSameNamespaceAs<ViewBase>(this))
{
MainViewModel.addView(v);
}

Iterate through settings files

I'm currently working on a VSTO project for which I have 5 .settings files:
Settings.settings (Default)
s201213.settings
s201314.settings
s201415.settings
s201516.settings
Over time there will be more settings files included following the same naming convention ('s' followed by a tax year).
I know I can iterate through a settings file, but is there a way to iterate through the actual settings files themselves?
I've tried things such as:
public void Example()
{
System.Collections.IEnumerator testSetting = MyAddIn.Properties.s201213.Default.Properties.GetEnumerator();
while (testSetting.MoveNext())
{
System.Diagnostics.Debug.WriteLine("Setting:\t" + testSetting.Current.ToString());
}
}
Which obviously only iterates through a single settings file, but I can't seem to figure out the logic of iterating through all the settings files as a collection, without explicitly naming each one in the code. I hope that makes sense, any help is appreciated.
Update:
I think I'm getting somewhere with the following code:
foreach(Type test in Assembly.GetExecutingAssembly().GetTypes())
{
if (System.Text.RegularExpressions.Regex.IsMatch(test.Name, "^s[0-9]{6}$"))
{
PropertyInfo value = test.GetProperty("LEL");
try
{
System.Diagnostics.Debug.WriteLine("Name:\t" + test.Name +
"\nNameSpace:\t" + test.Namespace +
"\nProperty:\t" + test.GetProperty("LEL").ToString() +
"\n");
}
catch(Exception e)
{
System.Diagnostics.Debug.WriteLine(e.Message);
}
}
}
Which seems to be recognising the settings files and the stored values:
Output:
Name: s201213
NameSpace: MyAddIn.Properties
Property: Double LEL
Name: s201314
NameSpace: MyAddIn.Properties
Property: Double LEL
Name: s201415
NameSpace: MyAddIn.Properties
Property: Double LEL
Name: s201516
NameSpace: MyAddIn.Properties
Property: Double LEL
However I can't seem to get the actual value of the "LEL" setting which should return a Double?
2nd Update
I've actually given up and decided to use a local DB instead - but I would still like to know if this is possible, and I think other people would like to know too so I'm going to throw a bounty at it to try and generate some interest.
Jeremy's answer got me to the finish post, but thought I'd post the final code I used so that it can be seen in context:
public void GetLEL()
{
var fileMap = new ConfigurationFileMap(AppDomain.CurrentDomain.BaseDirectory + #"CustomAddIn.dll.config");
var configuration = ConfigurationManager.OpenMappedMachineConfiguration(fileMap);
var sectionGroup = configuration.GetSectionGroup("userSettings");
var section = (ClientSettingsSection)sectionGroup.Sections.Get("MyAddIn.s201213");
var setting = section.Settings.Get("LEL");
System.Diagnostics.Debug.WriteLine(setting.Value.ValueXml.InnerXml);
// Prints "107" as expected.
}
The answer is really simple once you see it (no need to iterate through Types nor use System.IO directory):
using System.Configuration; //Add a reference to this DLL
...
var fileMap = new ConfigurationFileMap(Application.StartupPath + #"\GetSettingFilesValues.exe.config");
var configuration = ConfigurationManager.OpenMappedMachineConfiguration(fileMap);
var sectionGroup = configuration.GetSectionGroup("applicationSettings"); // This is the section group name, change to your needs, ie application or user
var section = (ClientSettingsSection)sectionGroup.Sections.Get("GetSettingFilesValues.Properties.s201415"); //This is the section name, change to your needs (you know the tax years you need)
var setting = section.Settings.Get("LEL");
System.Diagnostics.Debug.WriteLine(setting.Value.ValueXml.InnerXml);
I agree with your approach to use the dB, though I'm sure this will benefit other people too.
I believe you can use the System.IO namespace classes for iterating through files with the same extension. There is no built-in mechanisms or properties for that.

Duplicating content on save for a multilingual umbraco site

[Edit] I have actually been allowed to use the doc names, which makes it much easier but I still think it would be interesting to find out if it is possible.
I have to set a trigger to duplicate content to different branches on the content tree as the site will be in several languages. I have been told that I cannot access the documents by name(as they may change) and I shouldn't use node IDs either(not that I would know how to, after a while it would become difficult to follow the structure).
How can I traverse the tree to insert the new document in the relevant sub branches in the other languages? Is there a way?
You can use the Document.AfterPublish event to catch the specific document object after it's been published. I would use this event handler to check the node type alias is one that you want copied, then you can call Document.MakeNew and pass the node ID of the new location.
This means you don't have to use a specific node ID or document name to trap an event.
Example:
using umbraco.cms.businesslogic.web;
using umbraco.cms.businesslogic;
using umbraco.BusinessLogic;
namespace MyWebsite {
public class MyApp : ApplicationBase {
public MyApp()
: base() {
Document.AfterPublish += new Document.PublishEventHandler(Document_AfterPublish);
}
void Document_AfterPublish(Document sender, PublishEventArgs e) {
if (sender.ContentType.Alias == "DoctypeAliasOfDocumentYouWantToCopy") {
int parentId = 0; // Change to the ID of where you want to create this document as a child.
Document d = Document.MakeNew("Name of new document", DocumentType.GetByAlias(sender.ContentType.Alias), User.GetUser(1), parentId)
foreach (var prop in sender.GenericProperties) {
d.getProperty(prop.PropertyType.Alias).Value = sender.getProperty(prop.PropertyType.Alias).Value;
}
d.Save();
d.Publish(User.GetUser(1));
}
}
}
}

Categories