I'm trying to create a Markup Extension that will take a string of HTML, convert it to a FlowDocument, and return the FlowDocument. I'm fairly new to creating Markup Extensions and I'm hoping this will be obvious to someone with more experience. Here is my code:
[MarkupExtensionReturnType(typeof(FlowDocument))]
public class HtmlToXamlExtension : MarkupExtension
{
public HtmlToXamlExtension(String source)
{
this.Source = source;
}
[ConstructorArgument("source")]
public String Source { get; set; }
public Type LocalizationResourceType { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (this.Source == null)
{
throw new InvalidOperationException("Source must be set.");
}
FlowDocument flowDocument = new FlowDocument();
flowDocument.PagePadding = new Thickness(0, 0, 0, 0);
string xaml = HtmlToXamlConverter.ConvertHtmlToXaml(Source.ToString(), false);
using (MemoryStream stream = new MemoryStream((new ASCIIEncoding()).GetBytes(xaml)))
{
TextRange text = new TextRange(flowDocument.ContentStart, flowDocument.ContentEnd);
text.Load(stream, DataFormats.Xaml);
}
return flowDocument;
}
}
Update: Here is the XAML.
<RadioButton.ToolTip>
<FlowDocumentScrollViewer Document="{ext:HtmlToXaml Source={x:Static res:ExtrudeXaml.RadioButtonCreateBody_TooltipContent}}" ScrollViewer.VerticalScrollBarVisibility="Hidden" />
</RadioButton.ToolTip>
And my VS error list:
Error 3 Unknown property 'Source' for type 'MS.Internal.Markup.MarkupExtensionParser+UnknownMarkupExtension' encountered while parsing a Markup Extension. Line 89 Position 49.
Error 1 The type "HtmlToXamlExtension" does not include a constructor that has the specified number of arguments.
Error 2 No constructor for type 'HtmlToXamlExtension' has 0 parameters.
You implemented you MarkupExtension without default constructor:
So you have 2 options:
Delete your specific constructor (Anyway you set Source
directly)
Change invocation of you HtmlToXamlExtension if you remove Source= part, then Wpf will try to find constructor matching all unnamed fields right after ext:HtmlToXaml part:
<RadioButton.ToolTip>
<FlowDocumentScrollViewer
Document="{ext:HtmlToXaml {x:Static res:ExtrudeXaml.RadioButtonCreateBody_TooltipContent}}"
ScrollViewer.VerticalScrollBarVisibility="Hidden" />
</RadioButton.ToolTip>
UPD: Even though it works, but MSDN says, that you should have default constructor
Hope it helps.
You should create default constructor for your markup extension, and everything will be fine.
It helped to me to install the .NET 4.7 (Developer Pack), I've seen this bug at .NET 4.6 but after upgrading it's gone.
Related
I have a .NET assembly that is built by me but would like to be able rewrite the .DLL with some minor but arbitrary attribute change file at runtime. Specifically I would like to be able to change a property of an attribute of a class so that I can customize the binary depending on the situation.
To illustrate, I want to achieve the effect of editing the assembly being generated from the code
[SomeAttribute("Name")]
public class MyClass{
...
such that the new assembly is functionally the same as
[SomeAttribute("Custom Name")]
public class MyClass{
...
And this "Custom Name" could be anything (determined at runtime). Is this possible to do at runtime?
The reason why the actual .DLL needs to be modified is because it will get loaded up by a seperate process which cannot determine the runtime information (I do not control this process).
Experimentation so far has shown that it seems to work if the new "Custom Name" is the same length as the original, but not otherwise (even if you edit the preceding byte that specifies the length; presumably there are offsets stored in the file somewhere).
EDIT: Forgot to mention, solution needs to be under the .NET 2 framework as well.
Unclear what you really want to do (XY problem?)
Still, if you want to modify an assembly, you normally use Mono.Cecil that self-describes as: you can load existing managed assemblies, browse all the contained types, modify them on the fly and save back to the disk the modified assembly. .
Note that an attribute can contain extra data on top of the data that is passed as a parameter:
public class MyAttribute : Attribute
{
public MyAttribute(string str)
{
argument = str;
}
private string argument;
public string Argument { get; }
public string AssemblyName
{
get
{
return Assembly.GetEntryAssembly().FullName;
}
}
}
[MyAttribute("Hello")]
class Program
{
static void Main(string[] args)
{
var attr = typeof(Program).GetCustomAttribute<MyAttribute>();
Console.WriteLine(attr.Argument);
Console.WriteLine(attr.AssemblyName);
}
}
Using the extremely helpful suggestion from #xanatos I have made this solution:
Under .NET 2, you can install package Mono.Cecil 0.9.6.1.
The code then is as follows:
AssemblyDefinition assbDef = AssemblyDefinition.ReadAssembly("x.dll");
TypeDefinition type = assbDef.MainModule.GetType("NameSpace.MyClass").Resolve();
foreach (CustomAttribute attr in type.CustomAttributes)
{
TypeReference argTypeRef = null;
int? index = null;
for (int i = 0; i < attr.ConstructorArguments.Count; i++)
{
CustomAttributeArgument arg = attr.ConstructorArguments[i];
string stringValue = arg.Value as string;
if (stringValue == "Name")
{
argTypeRef = arg.Type;
index = i;
}
}
if (index != null)
{
attr.ConstructorArguments[(int)index] = new CustomAttributeArgument(argTypeRef, newName);
}
}
assbDef.Write("y.dll");
Which will search an assembly for any attribute arguments with value "Name" and replace their value with newName.
Rather than modifying the DLL, you can add attributes at run-time using the TypeDescriptor class; e.g.
TypeDescriptor.AddAttributes(typeof(MyClass), new SomeAttribute("Custom Name"));
The only caveat with this approach is that code which relies purely on reflection will not be able to read the added attribute(s) - you must use TypeDescriptor.GetAttributes(). However, most of the built-in .NET Framework methods that operate on attributes are aware of metadata added at run-time.
https://msdn.microsoft.com/en-us/library/system.componentmodel.typedescriptor_methods(v=vs.110).aspx
I found that C# didn't directly support ini files, so I went on the prowl and found the following library called ini-parser. The usage shows an extremely simple example, but for some reason I can't get Visual studio 2010 to like it. If I copy the following exactly from the wiki on their web page:
IniParser.FileIniDataParser parser = new FileIniDataParser();
IniData parsedData = parser.LoadFile("TestIniFile.ini");
I get the following error, with the parser part of parser.LoadFile() underlined and the following error:
Error 1 A field initializer cannot reference the non-static field,
method, or property
'WindowsFormsApplication1.Form1.parser' C:\Users\Support\Documents\Visual
Studio
2010\Projects\WindowsFormsApplication1\WindowsFormsApplication1\Form1.cs 28 30 WindowsFormsApplication1
I'm not sure what to make of what it's saying, or how to fix it. Can someone else offer up a suggestion/solution?
You're trying to do this in a field initializer. You're not allowed to refer to this within an instance field initalizer. Do it in the constructor instead:
private readonly IniData configuration;
public Form1()
{
InitializeComponent();
IniParser.FileIniDataParser parser = new FileIniDataParser();
configuration = parser.LoadFile("TestIniFile.ini");
}
Or just do it inline without a separate variable for the parser at all:
private readonly IniData configuration =
new FileIniDataParser().LoadFile("TestIniFile.ini");
(I'm assuming you don't actually need the parser for anything else, so it's pointless using a field for it.)
The error message is basically saying that you can't use parser (another field) within a field initializer of the class (the initializer for parsedData).
You have to put this logic in the constructor:
FileIniDataParser parser = new FileIniDataParser();
IniData parsedData;
public Form1()
{
InitializeComponent();
parsedData = parser.LoadFile("TestIniFile.ini");
}
Actual situation is, that I added to the MvxViewTypeResolver class the "Fragment"-Case, so it does look like this:
#region Copyright
// <copyright file="MvxViewTypeResolver.cs" company="Cirrious">
// (c) Copyright Cirrious. http://www.cirrious.com
// This source is subject to the Microsoft Public License (Ms-PL)
// Please see license.txt on http://opensource.org/licenses/ms-pl.html
// All other rights reserved.
// </copyright>
//
// Project Lead - Stuart Lodge, Cirrious. http://www.cirrious.com
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.Views;
using Cirrious.MvvmCross.Binding.Android.Interfaces.Binders;
namespace Cirrious.MvvmCross.Binding.Android.Binders
{
public class MvxViewTypeResolver : IMvxViewTypeResolver
{
private Dictionary<string, Type> _cache = new Dictionary<string, Type>();
public IDictionary<string, string> ViewNamespaceAbbreviations { get; set; }
#region IMvxViewTypeResolver Members
public virtual Type Resolve(string tagName)
{
Type toReturn;
if (_cache.TryGetValue(tagName, out toReturn))
return toReturn;
var unabbreviatedTagName = UnabbreviateTagName(tagName);
var longLowerCaseName = GetLookupName(unabbreviatedTagName);
var viewType = typeof(View);
#warning AppDomain.CurrentDomain.GetAssemblies is only the loaded assemblies - so we might miss controls if not already loaded
var query = from assembly in AppDomain.CurrentDomain.GetAssemblies()
from type in assembly.GetTypes()
where viewType.IsAssignableFrom(type)
where (type.FullName ?? "-").ToLowerInvariant() == longLowerCaseName
select type;
toReturn = query.FirstOrDefault();
_cache[tagName] = toReturn;
return toReturn;
}
private string UnabbreviateTagName(string tagName)
{
var filteredTagName = tagName;
if (ViewNamespaceAbbreviations != null)
{
var split = tagName.Split(new char[] {'.'}, 2, StringSplitOptions.RemoveEmptyEntries);
if (split.Length == 2)
{
var abbreviate = split[0];
string fullName;
if (ViewNamespaceAbbreviations.TryGetValue(abbreviate, out fullName))
{
filteredTagName = fullName + "." + split[1];
}
}
}
return filteredTagName;
}
#endregion
protected string GetLookupName(string tagName)
{
var nameBuilder = new StringBuilder();
switch (tagName)
{
case "View":
case "ViewGroup":
nameBuilder.Append("android.view.");
break;
case "fragment":
nameBuilder.Append("android.app.");
break;
default:
if (!IsFullyQualified(tagName))
nameBuilder.Append("android.widget.");
break;
}
nameBuilder.Append(tagName);
return nameBuilder.ToString().ToLowerInvariant();
}
private static bool IsFullyQualified(string tagName)
{
return tagName.Contains(".");
}
}
}
Now it is submitting the correct longLowerCaseTagName (android.app.fragment) but in the query it isn't able to resolve the type.
My suggestion is, that the fragment-control isn't loaded when the type should be resolved. Maybe there is an other way to get the type resolved?
Also if I add a custom type (giving the tag Mvx.MyCustomType in the axml) it doesn't get resolved. Do I have to add something in the MvxBindingAttributes.xml in this case?
Thanks for the help!
First an explanation of the code:
The custom XML inflater factory used by the MvvmCross Binder tries to load Views in a very similar way to the standard 2.x Android XML inflater.
The default code for the view type resolution is indeed in: https://github.com/slodge/MvvmCross/blob/master/Cirrious/Cirrious.MvvmCross.Binding/Android/Binders/MvxViewTypeResolver.cs
If your xml contains a name such as <MyCompany.MyProject.MyViews.MyFirstView /> then the view type resolver:
first checks for abbreviations and expands these into full namespaces - by default the only known abbreviation is Mvx. which is expanded to: Cirrious.MvvmCross.Binding.Android.Views.. If you want to add more abbrevations then override ViewNamespaceAbbreviations in https://github.com/slodge/MvvmCross/blob/master/Cirrious/Cirrious.MvvmCross.Binding/Android/MvxBaseAndroidBindingSetup.cs
then checks to see if the unabbreviated name is a non-namespaced name. If it is, then it assumes that the class is the Android namespace and prepends it with android.view. or android.widget.
then converts the fully namespaced name to all lowercase as a case-insensitive lookup key
uses that lowercase key to search all Types which derive from View in all loaded assemblies.
caches the result (whether its null or not) in order to speed up subsequent inflations.
All of this behaviour was designed to match the default Android xml view inflation code in http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.3.6_r1/android/view/LayoutInflater.java#LayoutInflater.createViewFromTag%28java.lang.String%2Candroid.util.AttributeSet%29
With that explanation out of the way - here's an answer to your questions:
MvvmCross does not yet currently contain any Fragment support. The official MonoDroid fragment support itself was only released last week, and I've not yet had anybody request fragments - Android "fragmentation" seems to have kept most people back on Activity and Dialog based code.
Briefly ;ooking at the documentation, fragment isn't an Android View - it looks like Fragment inherits directly from Java.Lang.Object - see http://developer.android.com/reference/android/app/Fragment.html
Because of this, there's no way that the MvvmCross ViewTypeResolver will currently work with fragments.
I would suggest that if you need both mvvmcross and fragments today, then your best bet is to replace the default resolver (using IoC) with your own resolver - but I can't offer much advice on this as I haven't yet fully read and understood the droid docs on http://developer.android.com/guide/topics/fundamentals/fragments.html
From my experience in creating the current inflation code, then I think you will find the source essential reading when you do this - e.g. see : http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.0.1_r1/android/view/LayoutInflater.java#LayoutInflater.createViewFromTag%28android.view.View%2Cjava.lang.String%2Candroid.util.AttributeSet%29
I can't give you any information on when official mvvmcross fragment support will be available - it's not something that is currently scheduled.
Custom views are supported, but will not normally live in the Mvx. abbreviated namespace.
They are much more likely to live in your UI application namespace, or in some shared library.
To see a custom view in action, see the PullToRefresh example in the tutorial - https://github.com/slodge/MvvmCross/blob/master/Sample%20-%20Tutorial/Tutorial/Tutorial.UI.Droid/Resources/Layout/Page_PullToRefreshView.axml
I've been storing collections of user settings in the Properties.Settings.Default object and using the Visual Studio settings designer (right-click on your project, click on Properties and then click on the Settings tab) to set the thing up. Recently, several users have complained that the data this particular setting tracks is missing, randomly.
To give an idea (not exactly how I do it, but somewhat close), the way it works is I have an object, like this:
class MyObject
{
public static string Property1 { get; set; }
public static string Property2 { get; set; }
public static string Property3 { get; set; }
public static string Property4 { get; set; }
}
Then in code, I might do something like this to save the information:
public void SaveInfo()
{
ArrayList userSetting = new ArrayList();
foreach (Something s in SomeCollectionHere) // For example, a ListView contains the info
{
MyObject o = new MyObject {
Property1 = s.P1;
Property2 = s.P2;
Property3 = s.P3;
Property4 = s.P4;
};
userSetting.Add(o);
}
Properties.Settings.Default.SettingName = userSetting;
}
Now, the code to pull it out is something like this:
public void RestoreInfo()
{
ArrayList setting = Properties.Settings.Default.SettingName;
foreach (object o in setting)
{
MyObject data = (MyObject)o;
// Do something with the data, like load it in a ListView
}
}
I've also made sure to decorate the Settings.Designer.cs file with [global::System.Configuration.SettingsSerializeAs(global::System.Configuration.SettingsSerializeAs.Binary)], like this:
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.SettingsSerializeAs(global::System.Configuration.SettingsSerializeAs.Binary)]
public global::System.Collections.ArrayList SettingName
{
get {
return ((global::System.Collections.ArrayList)(this["SettingName"]));
}
set {
this["SettingName"] = value;
}
}
Now, randomly, the information will disappear. I can debug this and see that Properties.Settings.Default is returning an empty ArrayList for SettingName. I would really rather not use an ArrayList, but I don't see a way to get a generic collection to store in this way.
I'm about to give up and save this information using plain XML on my own. I just wanted to verify that I was indeed pushing this bit of .NET infrastructure too far. Am I right?
I couldn't find an answer as to why these settings were disappearing and since I kept on happening, I ended up storing the complicated sets of settings separately in an XML file, manually serializing and deserializing them myself.
I had a very similar experience when using the SettingsSerializeAs Binary Attribute in the Settings Designer class. It worked in testing, but some time later it failed to restore the property values.
In my case there had been subsequent additions to the Settings made via the designer. Source control history showed that the SettingsSerializeAs Attribute had been removed from Settings.Designer.cs without my knowledge.
I added the following code to check that the attribute hadn't been accidentally lost it the equivalent of the RestoreInfo() method.
#if(DEBUG)
//Verify that the Property has the required attribute for Binary serialization.
System.Reflection.PropertyInfo binarySerializeProperty = Properties.Settings.Default.GetType().GetProperty("SettingName");
object[] customAttributes = binarySerializeProperty.GetCustomAttributes(typeof(System.Configuration.SettingsSerializeAsAttribute), false);
if (customAttributes.Length != 1)
{
throw new ApplicationException("SettingsSerializeAsAttribute required for SettingName property");
}
#endif
Also, only because it is missing from your example, don't forget to call Save. Say after calling SaveInfo().
Properties.Settings.Default.Save();
When using the Settings feature with the User scope, the settings are saved to the currently logged in user's Application Data (AppData in Vista/7) folder. So if UserA logged in, used your application, and then UserB logged in, he wouldn't have UserA's settings loaded, he would have his own.
In what you're trying to accomplish, I would suggest using the XmlSerializer class for serializing an list of objects. The use is pretty simple:
To serialize it:
ArrayList list = new ArrayList();
XmlSerializer s = new XmlSerializer(typeof(ArrayList));
using (FileStream fs = new FileStream(#"C:\path\to\settings.xml", FileMode.OpenOrCreate))
{
s.Serialize(fs, list);
}
To deserialize it:
ArrayList list;
XmlSerializer s = new XmlSerializer(typeof(ArrayList));
using (FileStream fs = new FileStream(#"C:\path\to\settings.xml", FileMode.Open))
{
list = (ArrayList)s.Deserialize(fs);
}
From your example I don't see anything incorrect about what your trying to do. I think the root of the problem your describing might be your assembly version changing? User settings do not auto-magically upgrade themselves (at least I couldn't get them to).
I can appreciate your plight, I went through this a few months ago. I coded up a UserSettings class that provided standard name/value pair collections (KeyValueConfigurationElement) under a named group heading something like the following:
<configSections>
<section name="userSettings" type="CSharpTest.Net.AppConfig.UserSettingsSection, CSharpTest.Net.Library"/>
</configSections>
<userSettings>
<add key="a" value="b"/>
<sections>
<section name="c">
<add key="a" value="y"/>
</section>
</sections>
</userSettings>
Anyway see if this meets your needs or provides some insight into implementing a custom ConfigurationSection to allow what you need.
Oh yea, the code is here:
http://csharptest.net/browse/src/Library/AppConfig
We save and read files by (de) serializing a class named "DocumentClass".
All was working well, untill we added 2 more fields to the documentclass. (We think that's the problem)
When we now try to open files that were serialized by the previous edition, we get an error.
System.ArgumentException: Object of type 'System.Int32' cannot be converted to type 'System.String'.
at SoftwareProject.Componenten.Bestand.DocumentClass.d(String A_0)
at de..ctor(String A_0)
at g.a(String A_0)
The method generating the error is the method "Read". (DocumentClass.d() is the obfuscated name )
But things get weirder: when we open the file in VS debug mode, no error is generated, but all fields in the documentclass are 0 or null ???
We are lost here ... please help ...
We've added the [OptionalField] attribute to the new fields, but that doesn't help ..
Why are all values null in debug mode ??
And where is the runtime error coming from ? How can we debug it ?
Thanks in advance!!
public static DocumentClass Read(string fullFilePath)
{
DocumentClass c = new DocumentClass();
Stream s = File.OpenRead(fullFilePath);
BinaryFormatter b = new BinaryFormatter();
//b.AssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple;
b.Binder = new MyCustomBinder();
try
{
c = (DocumentClass)b.Deserialize(s);
}
catch( Exception exc )
{
s.Close();
throw exc;
}
finally
{
s.Close();
}
return c;
}
public class MyCustomBinder : SerializationBinder {
public override Type BindToType(string assemblyName, string typeName) {
Type tyType = null;
string sShortAssemblyName = assemblyName.Split(',')[0];
Assembly[] ayAssemblies = AppDomain.CurrentDomain.GetAssemblies();
if (sShortAssemblyName.ToLower() == "debugAssemblyName")
{
sShortAssemblyName = "AppAssemblyName";
}
foreach (Assembly ayAssembly in ayAssemblies) {
if (sShortAssemblyName == ayAssembly.FullName.Split(',')[0]) {
tyType = ayAssembly.GetType(typeName);
break;
}
}
return tyType;
}
}
.Net has something called "Version Tolerant Serialization" which most likely solves this issue ;)
You should check out this easy to understand example on object serialization:
http://programming.flashadventures.com/c-sharp/writing-objects-to-files-serialization/
I assume you are using BinaryFormatter? This serializer is notoriously brittle, since it (by default) includes the field-name in the stream; this impacts obfuscation particularly badly. Presumably the obfuscator is now choosing new named for the fields (perhaps at random, perhaps due to the new fields), and so it can't deserialize correctly.
A few options:
don't obfuscate the DTO
implement ISerializable so the field names don't matter
use a serializer that doesn't care about field names
I'd personally opt for the latter, but I'm a bit biased ;-p I know of people using protobuf-net with obfuscated classes; the data only includes numeric markers, so the meaning isn't really exposed (except of course, by inspection of the data - post codes etc, but that is the job of encryption).
I think you need to use Custom Serialization