I have some auto-generated data being exported into my Unity project. To help me out I want to assign a custom icon to these assets to clearly identify them. This is of course simply possible via the editor itself, but ideally I'd like this to happen automatically on import.
To this effect I have written an AssetPostProcessor which should take care of this for me. In the example below (which applies to MonoScripts as an example but could apply to any kind of asset), all newly imported scripts will have the MyFancyIcon icon assigned to them. This update is both visible on the script assets themselves, as well as on the MonoBehaviours in the inspector.
using UnityEngine;
using UnityEditor;
using System.Reflection;
public class IconAssignmentPostProcessor : AssetPostprocessor
{
static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
{
Texture2D icon = AssetDatabase.LoadAssetAtPath<Texture2D>("Assets/Iconfolder/MyFancyIcon.png");
foreach (string asset in importedAssets)
{
MonoScript script = AssetDatabase.LoadAssetAtPath<MonoScript>(asset);
if(script != null)
{
PropertyInfo inspectorModeInfo = typeof(SerializedObject).GetProperty("inspectorMode", BindingFlags.NonPublic | BindingFlags.Instance);
SerializedObject serializedObject = new SerializedObject(script);
inspectorModeInfo.SetValue(serializedObject, InspectorMode.Debug, null);
SerializedProperty iconProperty = serializedObject.FindProperty("m_Icon");
iconProperty.objectReferenceValue = icon;
serializedObject.ApplyModifiedProperties();
serializedObject.Update();
EditorUtility.SetDirty(script);
}
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
}
And it works just fine, except for one problem. The updates aren't saved when closing the project and reopening it. To the best of my knowledge, either the EditorUtility.SetDirty(script); call should take care of this, or at the very least the AssetDatabase.SaveAssets(); call.
However, looking at the difference between manually assigning an icon (which works) and doing it programmatically, there is an icon field in the meta files associated with the assets which does get set when manually assigning an icon, but not in my scripted case. (In the scripted case the meta files aren't even updated)
So what gives? Do I have to do anything in particular when it's (apparently) only meta data I'm changing? Is there anything simple I'm overlooking?
Experimented with this code and concluded that this is a bug. Made contact with Unity and this is their reply:
Currently , it is a submitted bug and Our Developer Team is
investigating it. It seems that this bug seems to happen because of
AssetDatabes.SaveAssets() does not save changes.
The work around is to do this manually.
Processing and Saving Data when OnPostprocessAllAssets is called:
1.Create a Json file settings that will hold the settings if it does not exist.
2.When OnPostprocessAllAssets is called, load old Json file settings.
4.Apply fancy icon to the Asset.
5.Loop over the the loaded Json file settings and check if it contains the file from the importedAssets parameter.
If it contains the loaded file, modify that setting and save it. If it does not, add it to the List then save it.
6.Check if the asset importedAssets does not exist on the hard-drive with File.Exists. If it does not exist, remove it from the List of the loaded Json file settings then save it.
Auto re-apply the fancy icon when Unity loads:
1.Add a static constructor to the IconAssignmentPostProcessor class. This static constructor will automatically be called when Editor loads and also when OnPostprocessAllAssets is invoked.
2.When the constructor is called, create a Json file settings that will hold the settings if it does not exist.
3.Load the old Json file settings.
4.Re-apply the fancy icons by looping through the loaded Json file.
5.Check if the loaded Json file still have assets that is not on the drive. If so, remove that asset from the List then save it.
Below is what the new IconAssignmentPostProcessor script should look like:
using UnityEngine;
using UnityEditor;
using System.Reflection;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System;
public class IconAssignmentPostProcessor : AssetPostprocessor
{
// Called when Editor Starts
static IconAssignmentPostProcessor()
{
prepareSettingsDir();
reloadAllFancyIcons();
}
private static string settingsPath = Application.dataPath + "/FancyIconSettings.text";
private static string fancyIconPath = "Assets/Iconfolder/MyFancyIcon.png";
private static bool firstRun = true;
static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
{
prepareSettingsDir();
//Load old settings
FancyIconSaver savedFancyIconSaver = LoadSettings();
Texture2D icon = AssetDatabase.LoadAssetAtPath<Texture2D>(fancyIconPath);
for (int j = 0; j < importedAssets.Length; j++)
{
string asset = importedAssets[j];
MonoScript script = AssetDatabase.LoadAssetAtPath<MonoScript>(asset);
if (script != null)
{
//Apply fancy Icon
ApplyIcon(script, icon);
//Process each asset
processFancyIcon(savedFancyIconSaver, fancyIconPath, asset, pathToGUID(asset));
}
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
public static string pathToGUID(string path)
{
return AssetDatabase.AssetPathToGUID(path);
}
public static string guidToPath(string guid)
{
return AssetDatabase.GUIDToAssetPath(guid);
}
public static void processFancyIcon(FancyIconSaver oldSettings, string fancyIconPath, string scriptPath, string scriptGUID)
{
int matchIndex = -1;
if (oldSettings == null)
{
oldSettings = new FancyIconSaver();
}
if (oldSettings.fancyIconData == null)
{
oldSettings.fancyIconData = new List<FancyIconData>();
}
FancyIconData fancyIconData = new FancyIconData();
fancyIconData.fancyIconPath = fancyIconPath;
fancyIconData.scriptPath = scriptPath;
fancyIconData.scriptGUID = scriptGUID;
//Check if this guid exist in the List already. If so, override it with the match index
if (containsGUID(oldSettings, scriptGUID, out matchIndex))
{
oldSettings.fancyIconData[matchIndex] = fancyIconData;
}
else
{
//Does not exist, add it to the existing one
oldSettings.fancyIconData.Add(fancyIconData);
}
//Save the data
SaveSettings(oldSettings);
//If asset does not exist, delete it from the json settings
for (int i = 0; i < oldSettings.fancyIconData.Count; i++)
{
if (!assetExist(scriptPath))
{
//Remove it from the List then save the modified List
oldSettings.fancyIconData.RemoveAt(i);
SaveSettings(oldSettings);
Debug.Log("Asset " + scriptPath + " no longer exist. Deleted it from JSON Settings");
continue; //Continue to the next Settings in the List
}
}
}
//Re-loads all the fancy icons
public static void reloadAllFancyIcons()
{
if (!firstRun)
{
firstRun = false;
return; //Exit if this is not first run
}
//Load old settings
FancyIconSaver savedFancyIconSaver = LoadSettings();
if (savedFancyIconSaver == null || savedFancyIconSaver.fancyIconData == null)
{
Debug.Log("No Previous Fancy Icon Settings Found!");
return;//Exit
}
//Apply Icon Changes
for (int i = 0; i < savedFancyIconSaver.fancyIconData.Count; i++)
{
string asset = savedFancyIconSaver.fancyIconData[i].scriptPath;
//If asset does not exist, delete it from the json settings
if (!assetExist(asset))
{
//Remove it from the List then save the modified List
savedFancyIconSaver.fancyIconData.RemoveAt(i);
SaveSettings(savedFancyIconSaver);
Debug.Log("Asset " + asset + " no longer exist. Deleted it from JSON Settings");
continue; //Continue to the next Settings in the List
}
string tempFancyIconPath = savedFancyIconSaver.fancyIconData[i].fancyIconPath;
Texture2D icon = AssetDatabase.LoadAssetAtPath<Texture2D>(tempFancyIconPath);
MonoScript script = AssetDatabase.LoadAssetAtPath<MonoScript>(asset);
if (script == null)
{
continue;
}
Debug.Log(asset);
ApplyIcon(script, icon);
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
private static void ApplyIcon(MonoScript script, Texture2D icon)
{
PropertyInfo inspectorModeInfo = typeof(SerializedObject).GetProperty("inspectorMode", BindingFlags.NonPublic | BindingFlags.Instance);
SerializedObject serializedObject = new SerializedObject(script);
inspectorModeInfo.SetValue(serializedObject, InspectorMode.Debug, null);
SerializedProperty iconProperty = serializedObject.FindProperty("m_Icon");
iconProperty.objectReferenceValue = icon;
serializedObject.ApplyModifiedProperties();
serializedObject.Update();
EditorUtility.SetDirty(script);
Debug.Log("Applied Fancy Icon to: " + script.name);
}
//Creates the Settings File if it does not exit yet
private static void prepareSettingsDir()
{
if (!File.Exists(settingsPath))
{
File.Create(settingsPath);
}
}
public static void SaveSettings(FancyIconSaver fancyIconSaver)
{
try
{
string jsonData = JsonUtility.ToJson(fancyIconSaver, true);
Debug.Log("Data: " + jsonData);
byte[] jsonByte = Encoding.ASCII.GetBytes(jsonData);
File.WriteAllBytes(settingsPath, jsonByte);
}
catch (Exception e)
{
Debug.Log("Settings not Saved: " + e.Message);
}
}
public static FancyIconSaver LoadSettings()
{
FancyIconSaver loadedData = null;
try
{
byte[] jsonByte = File.ReadAllBytes(settingsPath);
string jsonData = Encoding.ASCII.GetString(jsonByte);
loadedData = JsonUtility.FromJson<FancyIconSaver>(jsonData);
return loadedData;
}
catch (Exception e)
{
Debug.Log("No Settings Loaded: " + e.Message);
}
return loadedData;
}
public static bool containsGUID(FancyIconSaver fancyIconSaver, string guid, out int matchIndex)
{
matchIndex = -1;
if (fancyIconSaver == null || fancyIconSaver.fancyIconData == null)
{
Debug.Log("List is null");
return false;
}
for (int i = 0; i < fancyIconSaver.fancyIconData.Count; i++)
{
if (fancyIconSaver.fancyIconData[i].scriptGUID == guid)
{
matchIndex = i;
return true;
}
}
return false;
}
public static bool assetExist(string path)
{
return File.Exists(path);
}
[Serializable]
public class FancyIconSaver
{
public List<FancyIconData> fancyIconData;
}
[Serializable]
public class FancyIconData
{
public string fancyIconPath;
public string scriptPath;
public string scriptGUID;
}
}
This should hold the fancy icons when Unity is restarted.
I think you may find your answer here: http://answers.unity3d.com/questions/344153/save-game-using-scriptable-object-derived-custom-a.html
Unfortunately.
Related
I am new to object-oriented programming and I am working on a small personal project with some SQL scripts.
I have a scenario where a SQL script calls a static method with a file path as input.
queries = Select Query from Table where Utils.ContainsKeyword(Query, #Path1) AND NOT Utils.ContainsKeyword(Query, #Path2);
I had initially created a static class that does the following:
public static class Utils
{
public static bool ContainsKeyword(string query, string path)
{
var isQueryInFile = false;
var stringFromFile = GetStringFromFile(path);
List<Regex>regexList = GetRegexList(stringFromFile);
if(regexList!= null)
{
isQueryInFile = regexList.Any(pattern => pattern.IsMatch(query));
}
return isQueryInFile;
}
private static string GetStringFromFile(string path)
{
var words = String.Empty;
if(!string.IsNullOrEmpty(path))
{
try
{
using (StreamReader sr = File.OpenText(path))
{
words = sr.ReadToEnd().Replace(Environment.Newline, "");
}
}
catch { return words; }
}
return words;
}
private static List<Regex> GetRegexList(string words)
{
if(string.IsNullOrEmpty(words)) { return null; }
return words.Split(',').Select(w=> new Regex(#"\b" + Regex.Escape(w) + #'\b', RegexOptions.Compiled | RegexOptions.IgnoreCase)).ToList();
}
}
My problem is that I neither want to read from the file every time the ContainsKeyword static method is called nor do I want to create a new RegexList every time. Also, I cannot change the SQL script and I have to send the path to the file as an input parameter for the method call in the SQL script since the path might change in the future.
Is there a way to make sure I only read the contents from the input path only once, store them in a string, and use the string for the match with different input queries?
To read the content only once, saving in memory will probaby be needed. Memory capacity could be an issue.
public Dictionary<string, string> FileContentCache { get; set; } // make sure that gets initialized
public string GetFileContentCache(string path)
{
if (FileContentCache == null) FileContentCache = new Dictionary<string, string>();
if (FileContentCache.ContainsKey(path))
return FileContentCache[path];
var fileData = GetStringFromFile(path);
FileContentCache.Add(path, fileData);
return fileData;
}
I am attempting to do a rough implementation of save data for usernames, for an android application made in Unity. This app will not be deployed and I don't need to worry about passwords for the usernames, so I am simply using the persistent data path to save a text file.
The entered usernames are added to a list of strings, and then used to populate a dropdown menu in my UI.
I am able to create the text file just fine, and save to it, and load from it. However, I am trying to implement a delete function that allows the user to delete a username they no longer wish to use, which should delete the username from the list of strings, remove it from the dropdown menu, as well as call the "Save" function that will re-save the new list to the same text file.
Currently, I have the user select the offending username from the dropdown, and then press a "Delete" button, which should use the index of the dropdown menu and delete the corresponding member in the list of strings using the "RemoveAt" function.
However, this seems to delete the entire list, and on opening the save text file, it is now empty. I believe there is something wrong with the way I am deleting the members of the list, or something wrong with the way the application re-saves the new list.
public class SignInManager : MonoBehaviour {
List<string> menuOptions = new List<string>();
//USER INPUT
public InputField newUserName;
public Dropdown selectedUsername;
public string userName;
public int dropDownOption;
//SAVING
FileInfo f;
// Use this for initialization
void Start () {
f = new FileInfo(Application.persistentDataPath + "save.txt");
//Load existing username options here
if (f.Exists)
{
Load();
}
Save();
Load();
}
//When the sign-in button is clicked
public void OnClick()
{
if (newUserName.text != "" ^ selectedUsername.value != 0)
{
//Save entered user data, later export to desired location
if (newUserName.text != "")
{
userName = newUserName.text;
selectedUsername.AddOptions(new List<string> { userName }); //AddOptions must take a list as parameter, but this is just a list of one each time
menuOptions.Add(userName);
Save();
Load();
//Save data to text file here
//Set current username to entered one
}
else if(selectedUsername.value != 0)
{
dropDownOption = selectedUsername.value; //Set current username to selected one
}
}
//When the user signs out
public void OnReturnClick()
{
newUserName.text = "";
selectedUsername.value = 0;
userName = "";
dropDownOption = 0;
}
}
public void DeleteOption()
{
if (selectedUsername.value != 0)
{
Debug.Log("Removing: " + selectedUsername.value);
//menuOptions.RemoveAt(selectedUsername.value);
selectedUsername.options.RemoveAt(selectedUsername.value);
selectedUsername.value = 0;
Save();
Load();
}
}
void Save()
{
StreamWriter w;
if (!f.Exists)
{
w = f.CreateText();
for (int i = 0; i < menuOptions.Count; i++)
{
w.WriteLine(menuOptions[i]);
}
}
else
{
f.Delete();
w = f.CreateText();
for (int i = 0; i < menuOptions.Count; i++)
{
w.WriteLine(menuOptions[i]);
}
}
w.Flush();
w.Close();
}
void Load()
{
if (f.Exists && selectedUsername != null) //don't know why selectedUsername sometimes comes up as null, but that additional condition was needed
{
selectedUsername.ClearOptions();
selectedUsername.AddOptions(new List<string> { "" });
StreamReader r = File.OpenText(Application.persistentDataPath + "save.txt");
string line;
while ((line = r.ReadLine()) != null)
{
menuOptions.Add(line);
selectedUsername.AddOptions(new List<string> { line });
}
r.Close();
}
}
}
}
So I tried this out myself. First off, I do not see how you are getting the whole list to disappear. That did not happen to me when I tested this in Unity. Maybe when your file is disappearing you are checking for it at the path C:\Users\Andy\AppData\LocalLow\{companyName}\{projectName}\save.txt If you are doing that, and you have a file at that location for some reason, then that file should be empty. You list your file as Application.persistantDataPath + "save.txt" and Application.persistantDataPath does not return an ending . So whether this s working how you think it should or not, it would be better for your file path to be Application.persistantDataPath + #"\save.txt"
Second, while I was not seeing the whole list dissappear, the list and file would not update. This is because you are doing
//menuOptions.RemoveAt(selectedUsername.value);
selectedUsername.options.RemoveAt(selectedUsername.value);
selectedUsername.value = 0;
While your save function does w.WriteLine(menuOptions[i]); You are making a change to the options, and then trying to save from the wrong list. Easy way to fix this is just it looked like you had it right the first time, uncomment //menuOptions.RemoveAt(selectedUsername.value); and remove selectedUsername.options.RemoveAt(selectedUsername.value); Afterall, your load should be making the changes to your dropdown.
Your load is also adding straight to the list, so everytime it is called, your list will get longer and longer. Your load should clear the list before it adds to it.
Also, I noticed in your save method, you are adding the name directly to the savelist and the dropdown list. This is the only reason you are seeing a change in the dropdown, as the load is not running because your f variable still thinks that the file does not exist. And your file alterations are all appending, so odd things happen to the files even when they do work. I suggest you use these changes I made:
void Start()
{
f = new FileInfo(Application.persistentDataPath + #"\save.txt");
//Load existing username options here
if (f.Exists)
{
Load();
}
Save();
Load();
}
void Save()
{
StreamWriter w = new StreamWriter(Application.persistentDataPath + #"\save.txt", false);
for (int i = 0; i < menuOptions.Count; i++)
{
w.WriteLine(menuOptions[i]);
}
w.Close();
}
void Load()
{
f = new FileInfo(Application.persistentDataPath + #"\save.txt");
if (f.Exists && selectedUsername != null) //don't know why selectedUsername sometimes comes up as null, but that additional condition was needed
{
selectedUsername.ClearOptions();
selectedUsername.AddOptions(new List<string> { "" });
StreamReader r = new StreamReader(Application.persistentDataPath + #"\save.txt");
string line;
menuOptions.Clear();
while ((line = r.ReadLine()) != null)
{
menuOptions.Add(line);
selectedUsername.AddOptions(new List<string> { line });
}
selectedUsername.RefreshShownValue();
r.Close();
}
}
I was able to create an XML serializer in Unity which saves my game state and was working fine. I haven't touched it since then, but a few days later it started saving the XML files on one line instead.
This was how it looked before:
<GameData xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<gameFlags/>
<memories/>
<lastSaveTime>-8586819040232916923</lastSaveTime>
<health>50</health>
<speed>0.27</speed>
<playerPosition>
<x>10</x>
<y>-48.2583</y>
<z>0</z>
</playerPosition>
<lastSaveFile/>
<savedScene>Area 0-0</savedScene>
<playerName>Jon</playerName>
<PlayerPositionX>10</PlayerPositionX>
<PlayerPositionY>-48.2583</PlayerPositionY>
<PlayerPositionZ>0</PlayerPositionZ>
</GameData>
This is how it looks now
-8586812865931894767 50 0.27 10 -48.2583 0 Default Area 0-0 Jon 10 -48.2583 0
Everything is all on one line. I reverted to my previous commit as well when it was working but it still is saving on one line. Does anyone have any experience with this change?
Save Game Method
public void SaveGame(string saveFile)
{
CheckDirectory();
gameData.generateGameData (GameObject.FindGameObjectWithTag("Player").GetComponent<Player>());
// Update saveFile name
if (saveFile == null)
{
saveFile = GenerateNewSaveName();
}
this.saveFile = saveFile;
// FileStream fs = File.Create(GameDic.Instance.SavePath + saveFile);
UpdateSaveData(saveFile);
string fullSavePath = SavePath + saveFile + FILE_EXTENSION;
FileStream fs;
// Create a file or open an old one up for writing to
if (!File.Exists(fullSavePath))
{
fs = File.Create(fullSavePath);
}
else
{
fs = File.OpenWrite(fullSavePath);
}
XmlSerializer serializer = new XmlSerializer(typeof(GameData));
TextWriter textWriter = new StreamWriter(fs, System.Text.Encoding.UTF8);
serializer.Serialize(textWriter, gameData);
fs.Close();
Debug.Log("Game Saved to " + fullSavePath);
}
GameData Class
[Serializable]
public class GameData
{
#region Public Fields
public List<GameFlag> gameFlags;
public List<string> memories;
public List<string> conversations;
public List<QuestInstance> questInstances;
public long lastSaveTime;
public float health, speed;
// Needs properties to access
[NonSerialized]
public Vector3 playerPosition;
public string lastSaveFile;
public string savedScene;
public string playerName;
#endregion Public Fields
#region Public Constructors
public void generateGameData(Player player) {
health = player.getStat (Stat.HP);
speed = player.getStat (Stat.SPEED);
savedScene = SceneManager.GetActiveScene ().name;
playerPosition = player.respawnPoint;
playerName = "Jon";
memories = player.GetComponentInChildren<MemoryManager> ().getAccessedMemories ();
conversations = player.GetComponentInChildren<NPCManager> ().getConversations ();
questInstances = player.GetComponent<QuestManager> ().getQuestInstances ();
}
public string GenerateNewSaveName()
{
int attempt = 0;
string newSaveName = "";
while (newSaveName == "")
{
// Save Name is Player Name
string checkString = gameData.playerName;
// Add a number if original already taken
if (attempt != 0) checkString += attempt;
if (!File.Exists(SavePath + checkString))
{
// Make the check string the new file name
newSaveName = checkString;
}
attempt++;
}
return newSaveName;
}
}
For the QuestInstance class I suppose it just can just be any filler class that's empty. It's serialized correctly.
Your problem is most likely this line:
fs = File.OpenWrite(fullSavePath);
If the previous file was longer than the new one, you will still have fragments from the previous save file at the end - that's why you can't deserialize it. Get rid of everything in that if condition except for the File.Create and it should work.
I have a problem with C# and Unity3D. I would like to read in a series of some to many JSON files from a single directory, and deserialise the data into one specific class for each file. I'm wondering if there's a fast way of looping that process so I don't need a large if/switch block or anything. Deserialisation is fine, but I'm having trouble actually assigning the data from each file to a list containing objects of the correct Type.
Note: The class name is the same as the filename. For example, if the filename is Cars.json, I want to find a Component called CarManager and use it to store the deserialised data in a List<Car> at CarManager.cars.
I'm inexperienced, and I don't really know how to work with Type references yet. If someone could explain how can I write the ProcessFile() method to successfully differentiate between Object types, so I can store the data for each file in Unity, I'd really appreciate it.
Cheers.
namespace Flight {
public class DatasetManager : MonoBehaviour {
private JsonSerializer serialiser;
private StreamReader streamReader;
private string path;
private string fileName;
private string extension;
public void Start() {
// Define Path
path = Application.dataPath + "/Data/";
extension = "json";
// Construct Serialiser
serialiser = new JsonSerializer();
serialiser.Converters.Add(new StringEnumConverter());
serialiser.NullValueHandling = NullValueHandling.Ignore;
// Import Data
Import();
}
private void Import() {
string[] files = Directory.GetFiles(path, "*." + extension);
if(files.Length == 0) return;
for(int i = 0; i < files.Length; i++) ProcessFile(files[i]);
}
private void ProcessFile(string xFile) {
streamReader = File.OpenText(xFile);
// Read Filename
string plural = Path.GetFileNameWithoutExtension(xFile);
string entity = plural.EndsWith("ies") ? plural.Substring(0,plural.Length-3) + "y" : plural.Substring(0,plural.Length-1);
string manager = entity + "Manager";
// Determine Entity & Manager Types
System.Type entityType = System.Type.GetType("Flight." + entity);
System.Type managerType = System.Type.GetType("Flight." + manager);
if(entityType == null || managerType == null) return;
// Determine List Type
System.Type listType = null;
listType = typeof(List<>).MakeGenericType(entityType);
if(listType == null) return;
// Acquire Data
List<dynamic> data = System.Activator.CreateInstance(listType) as List<dynamic>;
data = serialiser.Deserialize(streamReader, typeof(List<dynamic>)) as List<dynamic>;
if(data == null) return;
// Store Data in Game
GameObject theGame = GameObject.FindGameObjectWithTag("Game");
Component theComponent = theGame.GetComponent(manager);
FieldInfo field = managerType.GetField(plural.ToLower());
/*** How can I proceed from here? ***/
List<dynamic> theList = field.GetValue(theComponent) as List<dynamic>;
field.SetValue(theComponent, data);
}
}
}
The above code produces an ArgumentException:
System.Collections.Generic.List1[System.Object] cannot be converted to target type: System.Collections.Generic.List1[Flight.Car]
No worries, fixed it. The following modified code within ProcessFile() appears to work properly:
private void ProcessFile(string xFile) {
// ...
// ...
// Determine List Type
System.Type listType = typeof(List<>).MakeGenericType(entityType);
if(listType == null) return;
// Acquire Data
streamReader = File.OpenText(xFile);
object data = null;
data = System.Activator.CreateInstance(listType);
data = serialiser.Deserialize(streamReader, listType);
if(data == null) return;
// Store Data in Game
GameObject theGame = GameObject.FindGameObjectWithTag("Game");
Component theComponent = theGame.GetComponent(manager);
FieldInfo field = managerType.GetField(plural.ToLower());
field.SetValue(theComponent, data);
}
I read Excel files using OpenXml. all work fine but if the spreadsheet contains one cell that has an address mail and after it a space and another word, such as:
abc#abc.com abc
It throws an exception immediately at the opening of the spreadsheet:
var _doc = SpreadsheetDocument.Open(_filePath, false);
exception:
DocumentFormat.OpenXml.Packaging.OpenXmlPackageException
Additional information:
Invalid Hyperlink: Malformed URI is embedded as a
hyperlink in the document.
There is an open issue on the OpenXml forum related to this problem: Malformed Hyperlink causes exception
In the post they talk about encountering this issue with a malformed "mailto:" hyperlink within a Word document.
They propose a work-around here: Workaround for malformed hyperlink exception
The workaround is essentially a small console application which locates the invalid URL and replaces it with a hard-coded value; here is the code snippet from their sample that does the replacement; you could augment this code to attempt to correct the passed brokenUri:
private static Uri FixUri(string brokenUri)
{
return new Uri("http://broken-link/");
}
The problem I had was actually with an Excel document (like you) and it had to do with a malformed http URL; I was pleasantly surprised to find that their code worked just fine with my Excel file.
Here is the entire work-around source code, just in case one of these links goes away in the future:
void Main(string[] args)
{
var fileName = #"C:\temp\corrupt.xlsx";
var newFileName = #"c:\temp\Fixed.xlsx";
var newFileInfo = new FileInfo(newFileName);
if (newFileInfo.Exists)
newFileInfo.Delete();
File.Copy(fileName, newFileName);
WordprocessingDocument wDoc;
try
{
using (wDoc = WordprocessingDocument.Open(newFileName, true))
{
ProcessDocument(wDoc);
}
}
catch (OpenXmlPackageException e)
{
e.Dump();
if (e.ToString().Contains("The specified package is not valid."))
{
using (FileStream fs = new FileStream(newFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
UriFixer.FixInvalidUri(fs, brokenUri => FixUri(brokenUri));
}
}
}
}
private static Uri FixUri(string brokenUri)
{
brokenUri.Dump();
return new Uri("http://broken-link/");
}
private static void ProcessDocument(WordprocessingDocument wDoc)
{
var elementCount = wDoc.MainDocumentPart.Document.Descendants().Count();
Console.WriteLine(elementCount);
}
}
public static class UriFixer
{
public static void FixInvalidUri(Stream fs, Func<string, Uri> invalidUriHandler)
{
XNamespace relNs = "http://schemas.openxmlformats.org/package/2006/relationships";
using (ZipArchive za = new ZipArchive(fs, ZipArchiveMode.Update))
{
foreach (var entry in za.Entries.ToList())
{
if (!entry.Name.EndsWith(".rels"))
continue;
bool replaceEntry = false;
XDocument entryXDoc = null;
using (var entryStream = entry.Open())
{
try
{
entryXDoc = XDocument.Load(entryStream);
if (entryXDoc.Root != null && entryXDoc.Root.Name.Namespace == relNs)
{
var urisToCheck = entryXDoc
.Descendants(relNs + "Relationship")
.Where(r => r.Attribute("TargetMode") != null && (string)r.Attribute("TargetMode") == "External");
foreach (var rel in urisToCheck)
{
var target = (string)rel.Attribute("Target");
if (target != null)
{
try
{
Uri uri = new Uri(target);
}
catch (UriFormatException)
{
Uri newUri = invalidUriHandler(target);
rel.Attribute("Target").Value = newUri.ToString();
replaceEntry = true;
}
}
}
}
}
catch (XmlException)
{
continue;
}
}
if (replaceEntry)
{
var fullName = entry.FullName;
entry.Delete();
var newEntry = za.CreateEntry(fullName);
using (StreamWriter writer = new StreamWriter(newEntry.Open()))
using (XmlWriter xmlWriter = XmlWriter.Create(writer))
{
entryXDoc.WriteTo(xmlWriter);
}
}
}
}
}
The fix by #RMD works great. I've been using it for years. But there is a new fix.
You can see the fix here in the changelog for issue #793
Upgrade OpenXML to 2.12.0.
Right click solution and select Manage NuGet Packages.
Implement the fix
It is helpful to have a unit test. Create an excel file with a bad email address like test#gmail,com. (Note the comma instead of the dot).
Make sure the stream you open and the call to SpreadsheetDocument.Open allows Read AND Write.
You need to implement a RelationshipErrorHandlerFactory and use it in the options when you open. Here is the code I used:
public class UriRelationshipErrorHandler : RelationshipErrorHandler
{
public override string Rewrite(Uri partUri, string id, string uri)
{
return "https://broken-link";
}
}
Then you need to use it when you open the document like this:
var openSettings = new OpenSettings
{
RelationshipErrorHandlerFactory = package =>
{
return new UriRelationshipErrorHandler();
}
};
using var document = SpreadsheetDocument.Open(stream, true, openSettings);
One of the nice things about this solution is that it does not require you to create a temporary "fixed" version of your file and it is far less code.
Unfortunately solution where you have to open file as zip and replace broken hyperlink would not help me.
I just was wondering how it is posible that it works fine when your target framework is 4.0 even if your only installed .Net Framework has version 4.7.2.
I have found out that there is private static field inside System.UriParser that selects version of URI's RFC specification. So it is possible to set it to V2 as it is set for .net 4.0 and lower versions of .Net Framework. Only problem that it is private static readonly.
Maybe someone will want to set it globally for whole application. But I wrote UriQuirksVersionPatcher that will update this version and restore it back in Dispose method. It is obviously not thread-safe but it is acceptable for my purpose.
using System;
using System.Diagnostics;
using System.Reflection;
namespace BarCap.RiskServices.RateSubmissions.Utility
{
#if (NET20 || NET35 || NET40)
public class UriQuirksVersionPatcher : IDisposable
{
public void Dispose()
{
}
}
#else
public class UriQuirksVersionPatcher : IDisposable
{
private const string _quirksVersionFieldName = "s_QuirksVersion"; //See Source\ndp\fx\src\net\System\_UriSyntax.cs in NexFX sources
private const string _uriQuirksVersionEnumName = "UriQuirksVersion";
/// <code>
/// private enum UriQuirksVersion
/// {
/// V1 = 1, // RFC 1738 - Not supported
/// V2 = 2, // RFC 2396
/// V3 = 3, // RFC 3986, 3987
/// }
/// </code>
private const string _oldQuirksVersion = "V2";
private static readonly Lazy<FieldInfo> _targetFieldInfo;
private static readonly Lazy<int?> _patchValue;
private readonly int _oldValue;
private readonly bool _isEnabled;
static UriQuirksVersionPatcher()
{
var targetType = typeof(UriParser);
_targetFieldInfo = new Lazy<FieldInfo>(() => targetType.GetField(_quirksVersionFieldName, BindingFlags.Static | BindingFlags.NonPublic));
_patchValue = new Lazy<int?>(() => GetUriQuirksVersion(targetType));
}
public UriQuirksVersionPatcher()
{
int? patchValue = _patchValue.Value;
_isEnabled = patchValue.HasValue;
if (!_isEnabled) //Disabled if it failed to get enum value
{
return;
}
int originalValue = QuirksVersion;
_isEnabled = originalValue != patchValue;
if (!_isEnabled) //Disabled if value is proper
{
return;
}
_oldValue = originalValue;
QuirksVersion = patchValue.Value;
}
private int QuirksVersion
{
get
{
return (int)_targetFieldInfo.Value.GetValue(null);
}
set
{
_targetFieldInfo.Value.SetValue(null, value);
}
}
private static int? GetUriQuirksVersion(Type targetType)
{
int? result = null;
try
{
result = (int)targetType.GetNestedType(_uriQuirksVersionEnumName, BindingFlags.Static | BindingFlags.NonPublic)
.GetField(_oldQuirksVersion, BindingFlags.Static | BindingFlags.Public)
.GetValue(null);
}
catch
{
#if DEBUG
Debug.WriteLine("ERROR: Failed to find UriQuirksVersion.V2 enum member.");
throw;
#endif
}
return result;
}
public void Dispose()
{
if (_isEnabled)
{
QuirksVersion = _oldValue;
}
}
}
#endif
}
Usage:
using(new UriQuirksVersionPatcher())
{
using(var document = SpreadsheetDocument.Open(fullPath, false))
{
//.....
}
}
P.S. Later I found that someone already implemented this pathcher: https://github.com/google/google-api-dotnet-client/blob/master/Src/Support/Google.Apis.Core/Util/UriPatcher.cs
I haven't use OpenXml but if there's no specific reason for using it then I highly recommend LinqToExcel from LinqToExcel. Example of code is here:
var sheet = new ExcelQueryFactory("filePath");
var allRows = from r in sheet.Worksheet() select r;
foreach (var r in allRows) {
var cella = r["Header"].ToString();
}