I am building a game with XNA, and I have a custom file format for my game's levels. I want to load them and parse them myself, without using XNA's content pipeline. I have this much working, and by adding the files to the Content project I can even edit them in Visual Studio (which I also want).
The Problem: I get a warning stating "Project item 'item.lvl' was not built with the XNA Framework Content Pipeline. Set its Build Action property to Compile to build it."
I do not want XNA to Compile it, since I am doing my own parsing. How can I disable the warning?
Set the file's Build Action to None, and then set it to Copy if newer. That will cause the file to be written to the proper output directory without putting it through the Content Pipeline.
The solution could be create a custom content importer as explained here: Creating a Custom Importer and Processor. To create a simple content importer you have to inherit your class from the ContentImporter<T> (abstract class) and override the Import() method.
Here is a simple example from the msdn:
//...
using Microsoft.Xna.Framework.Content.Pipeline;
class PSSourceCode
{
const string techniqueCode = "{ pass p0 { PixelShader = compile ps_2_0 main(); } }";
public PSSourceCode(string sourceCode, string techniqueName)
{
this.sourceCode = sourceCode + "\ntechnique " + techniqueName + techniqueCode;
}
private string sourceCode;
public string SourceCode { get { return sourceCode; } }
}
[ContentImporter(".psh", DefaultProcessor = "PSProcessor", DisplayName = "Pixel Shader Importer")]
class PSImporter : ContentImporter<PSSourceCode>
{
public override PSSourceCode Import(string filename,
ContentImporterContext context)
{
string sourceCode = System.IO.File.ReadAllText(filename);
return new PSSourceCode(sourceCode, System.IO.Path.GetFileNameWithoutExtension(filename));
}
}
Related
We would like to distribute our project with assembly files instead of .cs scripts.
We thought that this would be easy thanks to assembly definition files, as unity is creating assembly files for the scripts they refer to anyway.
It turns out that when removing the .cs files and putting the assemblies, we ran into a problem :
The monobehaviors defined in the assemblies (so previously in our scripts) can't be added manually to a scene :
"Can't add script component xxx because the script class cannot be found"
While if we add the component through script (i.e. AddComponent) it works.
I'm using Unity 2017.3.f1 to generate the assembly files
Is there a trick to make this work ? or should I try to generate the assemblies using another approach ?
OP here.
Short answer is : don't keep both asmdef and assembly files. Remove the asmdef file if you replace the scripts with the generated assembly
What I ended up doing is the roughly following (this was for CI purpose):
First, we need to make sure Unity compiles the assembly file. So I have a GenerateAssemblies.cs file in an Editor folder that can be executed from command line:
GenerateAssemblies.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;
[InitializeOnLoad]
public static class GenerateAssemblies
{
private static string BATCH_MODE_PARAM = "-batchmode";
private const string REPLACE_ASSEMBLY_PARAM = "-replaceassembly";
static GenerateAssemblies()
{
List<String> args = Environment.GetCommandLineArgs().ToList();
if (args.Any(arg => arg.ToLower().Equals(BATCH_MODE_PARAM)))
{
Debug.LogFormat("GenerateAssemblies will try to parse the command line to replace assemblies.\n" +
"\t Use {0} \"assemblyname\" for every assembly you wish to replace"
, REPLACE_ASSEMBLY_PARAM);
}
if (args.Any(arg => arg.ToLower().Equals(REPLACE_ASSEMBLY_PARAM))) // is a replacement requested ?
{
int lastIndex = 0;
while (lastIndex != -1)
{
lastIndex = args.FindIndex(lastIndex, arg => arg.ToLower().Equals(REPLACE_ASSEMBLY_PARAM));
if (lastIndex >= 0 && lastIndex + 1 < args.Count)
{
string assemblyToReplace = args[lastIndex + 1];
if (!assemblyToReplace.EndsWith(ReplaceAssemblies.ASSEMBLY_EXTENSION))
assemblyToReplace = assemblyToReplace + ReplaceAssemblies.ASSEMBLY_EXTENSION;
ReplaceAssemblies.instance.AddAssemblyFileToReplace(assemblyToReplace);
Debug.LogFormat("Added assembly {0} to the list of assemblies to replace.", assemblyToReplace);
lastIndex++;
}
}
CompilationPipeline.assemblyCompilationFinished += ReplaceAssemblies.instance.ReplaceAssembly; /* This serves as callback after Unity as compiled an assembly */
Debug.Log("Forcing recompilation of all scripts");
// to force recompilation
PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Standalone, PlayerSettings.GetScriptingDefineSymbolsForGroup(BuildTargetGroup.Standalone) + ";DUMMY_SYMBOL");
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
}
}
}
Then I have a ReplaceAssemblies.cs file in an editor folder that will :
find the assembly file correpsonding to the asmdef file
save the guid/classes correspondance of the script files
move the script files in a temporary folder
move the assembly in the same folder as the asmdef file
move the asmdef to a temporary folder
Replace the Guid and File ID values for each script in the assembly (to avoid breaking references in scenes and prefabs)
ReplaceAssemblies.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;
public class ReplaceAssemblies : ScriptableSingleton<ReplaceAssemblies>
{
public static string ASSEMBLY_EXTENSION = ".dll";
public static string ASSEMBLY_DEFINITION_EXTENSION = ".asmdef";
[SerializeField]
private List<String> assembliesFilesToReplace = new List<string>();
[SerializeField]
private List<string> pathsOfAssemblyFilesInAssetFolder = new List<string>();
[SerializeField]
private List<string> pathsOfAssemblyFilesCreatedByUnity = new List<string>();
[SerializeField]
private string tempSourceFilePath;
private static readonly string[] fileListPath = { "*.prefab", "*.unity", "*.asset" };
public string TempSourceFilePath
{
get
{
if (String.IsNullOrEmpty(tempSourceFilePath))
{
tempSourceFilePath = FileUtil.GetUniqueTempPathInProject();
}
return tempSourceFilePath;
}
}
void OnEnable()
{
Debug.Log("temp dir : " + TempSourceFilePath);
}
public void ReplaceAssembly(string assemblyPath, CompilerMessage[] messages)
{
string assemblyFileName = assembliesFilesToReplace.Find(assembly => assemblyPath.EndsWith(assembly));
// is this one of the assemblies we want to replace ?
if (!String.IsNullOrEmpty(assemblyFileName))
{
string[] assemblyDefinitionFilePaths = Directory.GetFiles(".", Path.GetFileNameWithoutExtension(assemblyFileName) + ASSEMBLY_DEFINITION_EXTENSION, SearchOption.AllDirectories);
if (assemblyDefinitionFilePaths.Length > 0)
{
string assemblyDefinitionFilePath = assemblyDefinitionFilePaths[0];
ReplaceAssembly(assemblyDefinitionFilePath);
}
}
}
public void AddAssemblyFileToReplace(string assemblyFile)
{
assembliesFilesToReplace.Add(assemblyFile);
}
private void ReplaceAssembly(string assemblyDefinitionFilePath)
{
Debug.LogFormat("Replacing scripts for assembly definition file {0}", assemblyDefinitionFilePath);
string asmdefDirectory = Path.GetDirectoryName(assemblyDefinitionFilePath);
string assemblyName = Path.GetFileNameWithoutExtension(assemblyDefinitionFilePath);
Assembly assemblyToReplace = CompilationPipeline.GetAssemblies().ToList().Find(assembly => assembly.name.ToLower().Equals(assemblyName.ToLower()));
string assemblyPath = assemblyToReplace.outputPath;
string assemblyFileName = Path.GetFileName(assemblyPath);
string[] assemblyFilePathInAssets = Directory.GetFiles("./Assets", assemblyFileName, SearchOption.AllDirectories);
// save the guid/classname correspondance of the scripts that we will remove
Dictionary<string, string> oldGUIDToClassNameMap = new Dictionary<string, string>();
if (assemblyFilePathInAssets.Length <= 0)
{
// Move all script files outside the asset folder
foreach (string sourceFile in assemblyToReplace.sourceFiles)
{
string tempScriptPath = Path.Combine(TempSourceFilePath, sourceFile);
Directory.CreateDirectory(Path.GetDirectoryName(tempScriptPath));
if (!File.Exists(sourceFile))
Debug.LogErrorFormat("File {0} does not exist while the assembly {1} references it.", sourceFile, assemblyToReplace.name);
Debug.Log("will move " + sourceFile + " to " + tempScriptPath);
// save the guid of the file because we may need to replace it later
MonoScript monoScript = AssetDatabase.LoadAssetAtPath<MonoScript>(sourceFile);
if (monoScript != null && monoScript.GetClass() != null)
oldGUIDToClassNameMap.Add(AssetDatabase.AssetPathToGUID(sourceFile), monoScript.GetClass().FullName);
FileUtil.MoveFileOrDirectory(sourceFile, tempScriptPath);
}
Debug.Log("Map of GUID/Class : \n" + String.Join("\n", oldGUIDToClassNameMap.Select(pair => pair.Key + " : " + pair.Value).ToArray()));
string finalAssemblyPath = Path.Combine(asmdefDirectory, assemblyFileName);
Debug.Log("will move " + assemblyPath + " to " + finalAssemblyPath);
FileUtil.MoveFileOrDirectory(assemblyPath, finalAssemblyPath);
string tempAsmdefPath = Path.Combine(TempSourceFilePath, Path.GetFileName(assemblyDefinitionFilePath));
Debug.Log("will move " + assemblyDefinitionFilePath + " to " + tempAsmdefPath);
FileUtil.MoveFileOrDirectory(assemblyDefinitionFilePath, tempAsmdefPath);
// Rename the asmdef meta file to the dll meta file so that the dll guid stays the same
FileUtil.MoveFileOrDirectory(assemblyDefinitionFilePath + ".meta", finalAssemblyPath + ".meta");
pathsOfAssemblyFilesInAssetFolder.Add(finalAssemblyPath);
pathsOfAssemblyFilesCreatedByUnity.Add(assemblyPath);
// We need to refresh before accessing the assets in the new assembly
AssetDatabase.Refresh();
// We need to remove .\ when using LoadAsslAssetsAtPath
string cleanFinalAssemblyPath = finalAssemblyPath.Replace(".\\", "");
var assetsInAssembly = AssetDatabase.LoadAllAssetsAtPath(cleanFinalAssemblyPath);
// list all components in the assembly file.
var assemblyObjects = assetsInAssembly.OfType<MonoScript>().ToArray();
// save the new GUID and file ID for the MonoScript in the new assembly
Dictionary<string, KeyValuePair<string, long>> newMonoScriptToIDsMap = new Dictionary<string, KeyValuePair<string, long>>();
// for each component, replace the guid and fileID file
for (var i = 0; i < assemblyObjects.Length; i++)
{
long dllFileId;
string dllGuid = null;
if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(assemblyObjects[i], out dllGuid, out dllFileId))
{
string fullClassName = assemblyObjects[i].GetClass().FullName;
newMonoScriptToIDsMap.Add(fullClassName, new KeyValuePair<string, long>(dllGuid, dllFileId));
}
}
Debug.Log("Map of Class/GUID:FILEID : \n" + String.Join("\n", newMonoScriptToIDsMap.Select(pair => pair.Key + " : " + pair.Value.Key + " - " + pair.Value.Value).ToArray()));
ReplaceIdsInAssets(oldGUIDToClassNameMap, newMonoScriptToIDsMap);
}
else
{
Debug.Log("Already found an assembly file named " + assemblyFileName + " in asset folder");
}
}
/// <summary>
/// Replace ids in all asset files using the given maps
/// </summary>
/// <param name="oldGUIDToClassNameMap">Maps GUID to be replaced => FullClassName</param>
/// <param name="newMonoScriptToIDsMap">Maps FullClassName => new GUID, new FileID</param>
private static void ReplaceIdsInAssets(Dictionary<string, string> oldGUIDToClassNameMap, Dictionary<string, KeyValuePair<string, long>> newMonoScriptToIDsMap)
{
StringBuilder output = new StringBuilder("Report of replaced ids : \n");
// list all the potential files that might need guid and fileID update
List<string> fileList = new List<string>();
foreach (string extension in fileListPath)
{
fileList.AddRange(Directory.GetFiles(Application.dataPath, extension, SearchOption.AllDirectories));
}
foreach (string file in fileList)
{
string[] fileLines = File.ReadAllLines(file);
for (int line = 0; line < fileLines.Length; line++)
{
//find all instances of the string "guid: " and grab the next 32 characters as the old GUID
if (fileLines[line].Contains("guid: "))
{
int index = fileLines[line].IndexOf("guid: ") + 6;
string oldGUID = fileLines[line].Substring(index, 32); // GUID has 32 characters.
if (oldGUIDToClassNameMap.ContainsKey(oldGUID) && newMonoScriptToIDsMap.ContainsKey(oldGUIDToClassNameMap[oldGUID]))
{
fileLines[line] = fileLines[line].Replace(oldGUID, newMonoScriptToIDsMap[oldGUIDToClassNameMap[oldGUID]].Key);
output.AppendFormat("File {0} : Found GUID {1} of class {2}. Replaced with new GUID {3}.", file, oldGUID, oldGUIDToClassNameMap[oldGUID], newMonoScriptToIDsMap[oldGUIDToClassNameMap[oldGUID]].Key);
if (fileLines[line].Contains("fileID: "))
{
index = fileLines[line].IndexOf("fileID: ") + 8;
int index2 = fileLines[line].IndexOf(",", index);
string oldFileID = fileLines[line].Substring(index, index2 - index); // GUID has 32 characters.
fileLines[line] = fileLines[line].Replace(oldFileID, newMonoScriptToIDsMap[oldGUIDToClassNameMap[oldGUID]].Value.ToString());
output.AppendFormat("Replaced fileID {0} with {1}", oldGUID, newMonoScriptToIDsMap[oldGUIDToClassNameMap[oldGUID]].Value.ToString());
}
output.Append("\n");
}
}
}
//Write the lines back to the file
File.WriteAllLines(file, fileLines);
}
Debug.Log(output.ToString());
}
[MenuItem("Tools/Replace Assembly")]
public static void ReplaceAssemblyMenu()
{
string assemblyDefinitionFilePath = EditorUtility.OpenFilePanel(
title: "Select Assembly Definition File",
directory: Application.dataPath,
extension: ASSEMBLY_DEFINITION_EXTENSION.Substring(1));
if (assemblyDefinitionFilePath.Length == 0)
return;
instance.ReplaceAssembly(assemblyDefinitionFilePath);
}
}
I was experiencing this issue, and like you, I was using the information provided from asmdef files to provide all the required information (which .cs files, what references, defines, etc) to build an assembly.
I found that the issue was the DLL I was creating had the same name as the asmdef file I was using to provide the information. Even though the asmdef file was no longer being compiled (because all the scripts had been removed to build the DLL), it was still interfering with the project.
So for me, the inconsistency between accessing a script from inside the editor and from inside scripts was because there was a DLL and as asmdef file with the same name in the project.
Giving the compiled DLL a different name or removing the asmdef file was the solution for me.
Just tested with Unity 2019.3.0b1.
Content of test class:
using System.Reflection;
using UnityEngine;
namespace Assets.Test
{
public class TestBehaviour : MonoBehaviour
{
void Start()
{
Debug.Log(Assembly.GetAssembly(GetType()));
}
}
}
First project with source code and assembly definition file
Second project with the generated DLL, working as expected
As far as i'm concerned, the use of asmdef merely forces unity3d to compile your scripts into separate assemblies that are then referenced by your project.
Actually, it creates projects in your unity solution that contain your .cs files and each of these projects is compiled into its own output assembly.
The error you are seeing might be related to assembly caching.
I've had that error a few months ago and it was due to an outdated assembly still being cached.
As a result, unity3d editor kinda hiccuped when loading the project and therefore could not load the specific assembly.
I fixed it by deleting the directories Library, obj and Temp and then reloaded the unity3d project.
To get rid of that for good, we have moved away from asmdef and .cs files inside our unity projects once and for all.
All our scripts have been extracted to separate projects that are never touched by unity3d.
Every project references UnityEngine.dll and/or UnityEditor.dll (for Editor assemblies) depending on which unity3d types it may require.
The projects are built locally using Visual Studio or server side in our CI pipeline.
Output is copied manually into the assets directory of a unity project where it is then loaded from within unity3d editor.
This last step is a pain admittedly but i have yet to find time to streamline this process some more.
This approach has a few benefits
We are in control of our code in one single repository.
There is only one single point of truth and every developer commits changes onto the same code base.
There are no copies of .cs files across any number of unity projects that consume our types.
There is no need to figure out merge conflicts from updating a unitypackage where there have been deletions.
Unit tests can be done server side (CI pipeline) without the need of some docker image with unity3d on top (ofc there are restrictions on how much you can test without the entire unity3d environment running).
We create our own NuGet packages that can be referenced in projects (vcproj, not unity projects!).
Types deriving from MonoBehaviour can be added to GameObjects via code or via unity3d editor.
You also get to explore loaded assemblies inside your unity3d editor project view by clicking on the arrow of an assembly which will expand to show the list of contained relevant types.
Let's talk about downsides
One example is that we use SteamVR for interacting with controls.
The SteamVR plugin for unity3d is released through unity's asset store and annoyingly it contains script files and resources but no assemblies.
This goes for pretty much all assets in the store by the way.
Since we can't build against code, we have to go through the trouble of compiling SteamVR once and then copy the output assembly somewhere else.
This is not just as tedious as a task can be, it also has some limitations of its own which i get to later.
Anyway, this lets us reference a compiled asset with our own code so we get to use asset specific types like SteamVR_Action in our code without having to use unity3d editor and script files in unity projects (or reflection which would be even worse).
Limitations of compiled assets like this are two fold.
For once, it is horribly inefficient to get there in the first place.
On the other hand, you'll only have to do that once for every version of an asset.
Once that's done, make it a private NuGet package and you're golden.
The other limitation is the way how unity3d approaches dependency injection.
Actually i'm not entirely sure what it really is they try to do but here goes.
Unity3d wants you to only ever reference assemblies from within ../UnityInstallDirectory/Editor/Data/Managed/.
In a perfect world, your own assemblies reference that big gunky UnityEngine.dll in this directory and once loaded by unity3d editor everything works as expected.
When you compile a unity project from within unity3d editor however, the resulting assembly references all the assemblies from within ../UnityInstallDirectory/Editor/Data/Managed/UnityEngine/ which contains a very small version of UnityEngine.dll which in turn acts as a type forwarder to all the other sub modules.
Not such a perfect world now is it?
Your previously compiled asset requires the type MonoBehaviour to sit in an assembly called UnityEngine.CoreModule.dll.
Your own project however expects it to sit in UnityEngine.dll since you're a good fellow and follow the rules.
This is just asking for trouble and to get around this problem we are now directly referencing all the managed sub modules from within ../UnityInstallDirectory/Editor/Data/Managed/UnityEngine/.
We also ignore unity3d editor moaning about how we are doing it wrong.
tl;dr
By doing all from above and leaving asmdef and .cs files out of the equation we are able to build, unit test and pack our logic and types into assemblies.
We are also able to keep a clean code base that can be easily maintained and extended without dozens of copies of the same code in multiple locations and/or repositories.
Why unity3d does things the way they do, i'll never understand.
I do know there is a thing called Building from HEAD but since the entirety of the .net ecosystem is using the binary format to share content in the form of referable assemblies, why would you want to do things differently?
This is a topic for another day though.
If you made it all the way through this post, i sincerely hope it is helping you fix your problem at hand.
In case i misinterpreted your question ... sorry :-)
Unity3d is weird ...
I am working on localization of texts across all the 5 projects which together forms the product.
Localization: So If user is from USA, they will see the product in en-US, if they are from China they will see the text in ch-CH.
And I am stuck at below stuff.
Each project will have its OWN bucket of Resx file (file where I am keeping for translations).
Project A - en-US.resx file
cn-CH.resx file
Project B - en-US.resx file
ch-CH.resx file
Project C - en-US.resx file
ch-CH.resx file
.
.
.
Now I have a Project Common which gets referenced by all the projects.
So What I wrote a singleton class in Common
public sealed class Translation
{
private static readonly Translation translation = new Translation();
public static Translation GetTranslation { get { return translation; } }
private Translation() { }
static Translation() { }
public string GetTranslatedMessage(string key, CultureInfo culture, string message, string namespace)
{
var rm = new ResourceManager("namespace", Assembly.GetExecutingAssembly());
message = rm.GetString(key, culture);
return message;
}
}
So far so good, As you can see I am using namespace as 4th parameter with resource manager so that I can look for the translation in the right project bucket , I just do something like below:
Translation.Translate(key, culture, message, namespace) // singleton class taking in the namespace to find the right bucket
And it works fine.
Question/Problem: But from every project I need to pass the namespace, I mean where ever I call I need to pass the namespace. I am wondering is there any way, I can implicitly tell which bucket each project needs to look into. Can I use Abstract or 2 singleton classes, factory may be?, or something like that. I am newbie so I am not familiar on how to tackle this issue. I just don't want to pass namespace in every call.
WorkAround: I can repeat this same singleton code in each project and get the stuff working, but then I will be repeating same singleton code in each project/
If you are open to a hack-y solution, and your various namespaced files are in folders named after the namespace, you could use the CallerFilePathAttribute and split the namespace out of the path. It does seem fragile:
Look up C# Caller Information. The sample they show is:
public void DoProcessing()
{
TraceMessage("Something happened.");
}
public void TraceMessage(string message,
[System.Runtime.CompilerServices.CallerMemberName] string memberName = "",
[System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "",
[System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
System.Diagnostics.Trace.WriteLine("message: " + message);
System.Diagnostics.Trace.WriteLine("member name: " + memberName);
System.Diagnostics.Trace.WriteLine("source file path: " + sourceFilePath);
System.Diagnostics.Trace.WriteLine("source line number: " + sourceLineNumber);
}
// Sample Output:
// message: Something happened.
// member name: DoProcessing
// source file path: c:\Users\username\Documents\Visual Studio 2012\Projects\CallerInfoCS\CallerInfoCS\Form1.cs
// source line number: 31
Then take what they have labeled as source file path and play path games to get the namespace. Parameters using these attributes are always optional (typically with they defaults set to default(T)). The compiler injects the value, the caller should not.
I am trying to load a html file that is in the same path as class i'm working with Via a web view in xamarin forms when I run the app I get a white screen and nothing loads here's my code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace App.Plan
{
public partial class Tornado : ContentPage
{
public Tornado()
{
InitializeComponent();
var browser = new WebView
{
Source = "local.html"
};
}
}
}
I realize this is old, but the documentation is maybe not entirely clear when it comes to local files so I thought I'd share my thoughts. Hopefully it's helpful to anyone who stumbles here.
The documentation states:
To display local content using a WebView, you'll need to open the HTML file like any other, then load the contents as a string into the Html property of an HtmlWebViewSource.
The key thing to note is that the WebView's Source property is ONLY for external URLs or HtmlWebViewSource. You cannot put a local URL in the Source property. The phrasing "you'll need to open the HTML file like any other" means (as it states shortly after) that you need to load the contents of the file from disk into a variable using a full file path, not URL.
The question then becomes "What about links to other local files?" That's where the BaseUrl property of HtmlWebViewSource comes into play. The documentation addresses this by stating the following:
Although the first page has been loaded, the WebView has no knowledge of where the HTML came from. That is a problem when dealing with pages that reference local resources. Examples of when that might happen include when local pages link to each other, a page makes use of a separate JavaScript file, or a page links to a CSS stylesheet.
In other words, any links to local resources in your HTML will be prepended with the BaseUrl by the Webview automatically.
To sum things up
Create the IBaseUrl Interface in your shared project
public interface IBaseUrl {
string Get();
}
Create the platform-specific implementations of IBaseUrl in each platform project
iOS:
[assembly: Dependency (typeof (BaseUrl_iOS))]
namespace WorkingWithWebview.iOS
{
public class BaseUrl_iOS : IBaseUrl
{
public string Get()
{
return NSBundle.MainBundle.BundlePath;
}
}
}
Android
[assembly: Dependency (typeof(BaseUrl_Android))]
namespace WorkingWithWebview.Android
{
public class BaseUrl_Android : IBaseUrl
{
public string Get()
{
return "file:///android_asset/";
}
}
}
UWP
[assembly: Dependency(typeof(BaseUrl))]
namespace WorkingWithWebview.UWP
{
public class BaseUrl : IBaseUrl
{
public string Get()
{
return "ms-appx-web:///";
}
}
}
Make sure your HTML files are in the appropriate folders and have the proper Build Action
iOS: Resources, build action: "BundleResource"
Android: Assets, build action: "AndroidAsset"
UWP: project root, build action: "content"
Make sure the WebView has a height and width request or it may not render:
It may be necessary to set the WidthRequest and HeightRequest properties of the WebView to see the HTML content, depending upon the layout the WebView is a child of. For example, this is required in a StackLayout.
Once that setup is done, you can put it into action in your shared project. Here's a simplified example:
// Use DI function to get the BaseUrl for the platform
var baseUrl = DependencyService.Get<IBaseUrl>().Get();
// You could append subfolders here if you don't
// want all the HTML files mixed with other resources:
// var baseUrl = System.IO.Path.Combine(DependencyService.Get<IBaseUrl>().Get(), "subfolder");
// Define the location of your initial HTML page using the base url
var initialHtmlPath = System.IO.Path.Combine(baseUrl, "index.html");
// Create the viewsource, loading the first HTML file as a string
var localHtmlViewSource = new HtmlWebViewSource();
localHtmlViewSource.BaseUrl = baseUrl;
localHtmlViewSource.Html = System.IO.File.ReadAllText(initialHtmlPath);
// Set the webview to use the local source
HelpWebView.Source = localHtmlViewSource;
Xamarin has docs in relation to this:
https://developer.xamarin.com/guides/xamarin-forms/user-interface/webview/
var browser = new WebView();
var htmlSource = new HtmlWebViewSource();
htmlSource.Html = #"<html><body>
<h1>Xamarin.Forms</h1>
<p>Welcome to WebView.</p>
</body></html>";
browser.Source = htmlSource;
Here is official sample in github
WorkingWithWebview
tabs.Children.Add (new LocalHtml {Title = "Local" });
tabs.Children.Add (new LocalHtmlBaseUrl {Title = "BaseUrl" });
tabs.Children.Add (new WebPage { Title = "Web Page"});
tabs.Children.Add (new WebAppPage {Title ="External"});
Second tab may help.
I have a web project like:
namespace Web
{
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
lbResult.Text = PathTest.GetBasePath();
}
}
}
The method PathTest.GetBasePath() is defined in another Project like:
namespace TestProject
{
public class PathTest
{
public static string GetBasePath()
{
return AppDomain.CurrentDomain.BaseDirectory;
}
}
}
Why it's display ...\Web\ while the TestProject assembly is compiled into bin folder(in other words it should display ...\Web\bin in my thought).
Now I got a troublesome if I modified method into:
namespace TestProject
{
public class FileReader
{
private const string m_filePath = #"\File.config";
public static string Read()
{
FileStream fs = null;
fs = new FileStream(AppDomain.CurrentDomain.BaseDirectory + m_filePath,FileMode.Open, FileAccess.Read);
StreamReader reader = new StreamReader(fs);
return reader.ReadToEnd();
}
}
}
The File.config is created in TestProject. Now AppDomain.CurrentDomain.BaseDirectory + m_filePath will returen ..\Web\File.config (actually the file was be copied into ..\Web\bin\File.config), an exception will be thrown.
You could say that I should modified m_filePath to #"\bin\File.config". However If I use this method in a Console app in your suggest, AppDomain.CurrentDomain.BaseDirectory + m_filePath will return ..\Console\bin\Debug\bin\File.config (actually the file was copyed into .\Console\bin\Debug\File.config), an exception will be thrown due to surplus bin.
In other words, in web app, AppDomain.CurrentDomain.BaseDirectory is a different path where file be copyed into (lack of /bin), but in console app it's the same one path.
Any one can help me?
Per MSDN, an App Domain "Represents an application domain, which is an isolated environment where applications execute." When you think about an ASP.Net application the root where the app resides is not the bin folder. It is totally possible, and in some cases reasonable, to have no files in your bin folder, and possibly no bin folder at all. Since AppDomain.CurrentDomain refers to the same object regardless of whether you call the code from code behind or from a dll in the bin folder you will end up with the root path to the web site.
When I've written code designed to run under both asp.net and windows apps usually I create a property that looks something like this:
public static string GetBasePath()
{
if(System.Web.HttpContext.Current == null) return AppDomain.CurrentDomain.BaseDirectory;
else return Path.Combine(AppDomain.CurrentDomain.BaseDirectory,"bin");
}
Another (untested) option would be to use:
public static string GetBasePath()
{
return System.Reflection.Assembly.GetExecutingAssembly().Location;
}
In case you want a solution that works for WinForms and Web Apps:
public string ApplicationPath
{
get
{
if (String.IsNullOrEmpty(AppDomain.CurrentDomain.RelativeSearchPath))
{
//exe folder for WinForms, Consoles, Windows Services
return AppDomain.CurrentDomain.BaseDirectory;
}
else
{
//bin folder for Web Apps
return AppDomain.CurrentDomain.RelativeSearchPath;
}
}
}
The above code snippet is for binaries locations.
The AppDomain.CurrentDomain.BaseDirectory is still a valid path for Web Apps, it's just the root folder where the web.config and Global.asax are, and is same as Server.MapPath(#"~\");
If you use AppDomain.CurrentDomain.SetupInformation.PrivateBinPath instead of BaseDirectory, then you should get the correct path.
When ASP.net builds your site it outputs build assemblies in its special place for them. So getting path in that way is strange.
For asp.net hosted applications you can use:
string path = HttpContext.Current.Server.MapPath("~/App_Data/somedata.xml");
I have these requirements coming from client every week for some new logic or verification. For which I have to code new logic (basically some if-else and loops) and launch a new build for him. I want to avoid it by simply coding my logic in visual studio then writing a utility to export it to XML or something and send it to client via e-mail. He just have to place this file in some appropriate folder and the application will behave considering this logic.
Please suggest some solutions. My platform is C# Asp.Net.
Thanks
Using .NET 4.6 and the NuGetPackage Microsoft.CodeAnalysis.Scripting you could implement a scripting engine to run your c# code residing in a textfile without building an assembly.
Install NuGet Package:
Install-Package Microsoft.CodeAnalysis.Scripting.CSharp
Implement TestClass with some basic C#-Code-Content:
class Program
{
static void Main(string[] args)
{
TestScript();
}
private static async void TestScript()
{
// Code snippet: a class with one string-property.
string codeContent = #" using System;
public class ScriptedClass
{
public string HelloWorld { get; set; }
public ScriptedClass()
{
HelloWorld = ""Hello Roslyn!"";
}
}
new ScriptedClass().HelloWorld";
// Instanciate CSharpScriptEngine
var engine = new CSharpScriptEngine();
// Execute code and return string property (HelloWorld)
var scriptingState = await engine.ExecuteAsync(codeContent);
// Print return value from CSharpScript
Console.WriteLine("Returned from CSharpScript: {0}", scriptingState.ReturnValue);
Console.WriteLine("Press any key to continue.");
Console.ReadKey();
}
}
Implement a ScriptingEngine:
internal sealed class CSharpScriptEngine
{
public async Task<ScriptState<object>> ExecuteAsync(string codeContent)
{
// Add references from calling assembly
ScriptOptions options = ScriptOptions.Default.AddReferences(Assembly.GetExecutingAssembly());
// Run codeContent with given options
return await CSharpScript.RunAsync(codeContent, options);
}
}
Read ScriptCode from textfile:
So basically you could read some csharpcode from a textfile of your choice and run them on the fly:
private static async void TestScript()
{
// Read in script file
string codeContent = File.ReadAllText(#"C:\Temp\CSharpScriptTest.cs");
var engine = new CSharpScriptEngine();
// Run script
var scriptingState = await engine.ExecuteAsync(codeContent);
Console.WriteLine("Returned from CSharpScript: {0}", scriptingState.ReturnValue);
Console.WriteLine("Press any key to continue.");
Console.ReadKey();
}
In case you are wondering how all of this works under the hood, Roslyn will create a so called submission from your script code. A submission is an in memory assembly containing the types generated around your script code, which can be identified among the assemblies in the current AppDomain by a ℛ prefix in the name.
The precise implementation details are not important here (though, for example, scriptcs heavily relies on understanding in detail how Roslyn works to provide its extra features), but it's important to know that submissions can be chained together. When they are chained, variables, methods or classes defined in an earlier submission are available to use in subsequent submissions, creating a feature of a C# REPL (read-evaluate-print loop).
C# and Visual Basic - Use Roslyn to Write a Live Code Analyzer for Your API
Hope it helps