|
xaml
在使用新的项目格式,可以使用 Target 添加项目,但是有一些项目需要在合适的时候添加,如果添加早了,那么会让用户看到这些文件,如果添加的时间是在引用编译之后,那么文件将无法进行编译。
如果你写了一个 MarkupExtension
在 XAML 当中使用,你会发现你在 MarkupExtension
中定时的属性是无法使用 XAML 绑定的,因为 MarkupExtension
不是一个 DependencyObject
。
本文将给出解决方案,让你能够在任意的类型中写出支持 XAML 绑定的属性;而不一定要依赖对象(DependencyObject
)和依赖属性(DependencyProperty
)。
下面是一个很简单的 MarkupExtension
,用户设置了什么值,就返回什么值。拿这么简单的类型只是为了避免额外引入复杂的理解难度。
public class WalterlvExtension : MarkupExtension
{
private object _value;
public object Value
{
get => _value;
set => _value = value;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return Value;
}
}
可以在 XAML 中直接赋值:
<Button Content="{local:Walterlv Value=blog.walterlv.com" />
但不能绑定:
<TextBox x:Name="SourceTextBox" Text="blog.walterlv.com" />
<Button Content="{local:Walterlv Value={Binding Text, Source={x:Reference SourceTextBox}}}" />
因为运行时会报错,提示绑定必须被设置到依赖对象的依赖属性中。在设计器中也可以看到提示不能绑定。
实际上这个问题是能够解决的(不过也花了我一些时间思考解决方案)。
既然绑定需要一个依赖属性,那么我们就定义一个依赖属性。非依赖对象中不能定义依赖属性,于是我们定义附加属性。
// 注意:这一段代码实际上是无效的。
public static readonly DependencyProperty ValueProperty = DependencyProperty.RegisterAttached(
"Value", typeof(object), typeof(WalterlvExtension), new PropertyMetadata(default(object)));
public object Value
{
get => ???.GetValue(ValueProperty);
set => ???.SetValue(ValueProperty, value);
}
这里问题来了,获取和设置附加属性是需要一个依赖对象的,那么我们哪里去找依赖对象呢?直接定义一个新的就好了。
于是我们定义一个新的依赖对象:
// 注意:这一段代码实际上是无效的。
public static readonly DependencyProperty ValueProperty = DependencyProperty.RegisterAttached(
"Value", typeof(object), typeof(WalterlvExtension), new PropertyMetadata(default(object)));
public object Value
{
get => _dependencyObject.GetValue(ValueProperty);
set => _dependencyObject.SetValue(ValueProperty, value);
}
private readonly DependencyObject _dependencyObject = new DependencyObject();
现在虽然可以编译通过,但是我们会遇到两个问题:
ValueProperty
的变更通知的回调函数中,我们只能找到 _dependencyObject
的实例,而无法找到外面的类型 WalterlvExtension
的实例;这几乎使得 Value
的变更通知完全失效。Value
的 set
方法中得到的 value
值是一个 Binding
对象,而不是正常依赖属性中得到的绑定的结果;这意味着我们无法直接使用 Value
的值。为了解决这两个问题,我必须自己写一个代理的依赖对象,用于帮助做属性的变更通知,以及处理绑定产生的 Binding
对象。在正常的依赖对象和依赖属性中,这些本来都不需要我们自己来处理。
于是我写了一个代理的依赖对象,我把它命名为 ClrBindingExchanger
,意思是将 CLR 属性和依赖属性的绑定进行交换。
代码如下:
public class ClrBindingExchanger : DependencyObject
{
private readonly object _owner;
private readonly DependencyProperty _attachedProperty;
private readonly Action<object, object> _valueChangeCallback;
public ClrBindingExchanger(object owner, DependencyProperty attachedProperty,
Action<object, object> valueChangeCallback = null)
{
_owner = owner;
_attachedProperty = attachedProperty;
_valueChangeCallback = valueChangeCallback;
}
public object GetValue()
{
return GetValue(_attachedProperty);
}
public void SetValue(object value)
{
if (value is Binding binding)
{
BindingOperations.SetBinding(this, _attachedProperty, binding);
}
else
{
SetValue(_attachedProperty, value);
}
}
public static void ValueChangeCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((ClrBindingExchanger) d)._valueChangeCallback?.Invoke(e.OldValue, e.NewValue);
}
}
这段代码的意思是这样的:
owner
参数完全没有用,我只是拿来备用,你可以删掉。attachedProperty
参数是需要定义的附加属性。
valueChangeCallback
参数是为了指定变更通知的,因为前面我们说变更通知不好做,于是就这样代理做变更通知。GetValue
和 SetValue
这两个方法是用来代替 DependencyObject
自带的 GetValue
和 SetValue
的,目的是执行我们希望特别执行的方法。SetValue
中我们需要自己考虑绑定对象,如果发现是绑定,那么就真的进行一次绑定。ValueChangeCallback
是给附加属性用的,因为用我的这种方法定义附加属性时,只能写出相同的代码,所以干脆就提取出来。而用法是这样的:
public class WalterlvExtension : MarkupExtension
{
public WalterlvExtension()
{
_valueExchanger = new ClrBindingExchanger(this, ValueProperty, OnValueChanged);
}
private readonly ClrBindingExchanger _valueExchanger;
public static readonly DependencyProperty ValueProperty = DependencyProperty.RegisterAttached(
"Value", typeof(object), typeof(WalterlvExtension),
new PropertyMetadata(null, ClrBindingExchanger.ValueChangeCallback));
public object Value
{
get => _valueExchanger.GetValue();
set => _valueExchanger.SetValue(value);
}
private void OnValueChanged(object oldValue, object newValue)
{
// 在这里可以处理 Value 属性值改变的变更通知。
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return Value;
}
}
对于一个属性来说,代码确实多了些,这实在是让人难受。可是,这可以达成目的呀!
解释一下:
_valueExchanger
,就是在使用我们刚刚写的那个新类。_valueExchanger
进行初始化,因为要传入 this
和一个实例方法 OnValueChanged
,所以只能在构造函数中初始化。ClrBindingExchanger.ValueChangeCallback
Value
GetValue
方法要换成我们自定义的 GetValue
哦SetValue
方法也要换成我们自定义的 SetValue
哦,这样绑定才可以生效OnValueChanged
就是我们实际的变更通知,这里得到的 oldValue
和 newValue
就是你期望的值,而不是我面前面奇怪的绑定实例。于是,绑定就这么在一个普通的类型和一个普通的 CLR 属性中生效了,而且还获得了变更通知。
参考资料
本文没有任何参考资料,所有方法都是我(walterlv)的原创方法,因为真的找不到资料呀!不过在找资料的过程中发现了一些没解决的文档或帖子:
作为一位 .NET 技术的死忠,开发 iOS 应用当然要使用 Xamarin 啦!
本文用我的阅读的文档和实践为素材,介绍如何使用 Xamarin 开发一个 iOS 的键盘扩展。
你可以在 Walterlv.CloudKeyboard 仓库中获得本文所述的全部源代码。
本文不会花篇幅来讲如何搭建 Xamarin iOS 开发的环境,不然这篇文章就没有重点。
于是,请阅读这一篇来了解如何搭建 Xamarin iOS 的开发环境:
了解以下背景知识,有助于我们接下来开发的时候少踩一些坑。当然我不会在这里说 iOS 应用开发的所有背景知识,只会说与 iOS 键盘扩展相关的部分。
这个不用太在意里面的实现,因为它只是我们的“容器项目”(前面有介绍)。实际上在本文我们完全不会碰这个项目里面的代码,只是为了配置我们的 iOS 应用包而已。未来你可以在这个容器应用里面做键盘的个性化设置。
然后,选择 iOS 平台。
我们只需要 iOS 端。因为对于键盘,不同系统的实现差异很大,之间共享的代码只能是非键盘部分的代码了。
当你创建完之后,你会看到三个不同的项目。
你可能发现 Walterlv.KeyboardExtension.Keyboard 项目有些奇怪,里面有 Main 函数和 AppDelegate,按道理这是一个主程序包。然而实际测试中单独有这个项目是跑不起来的(这可能是一个 Bug,如果修复了,请在下面评论或者邮件告知我,谢谢了)。
于是,Main 和 AppDelegate 这两个文件是可以删除的。如果你强迫症,就删掉吧。当然不删掉也不影响,不过我删掉了。
在 iOS 容器应用上面添加键盘扩展项目作为引用。
如果你感兴趣去查看 Walterlv.KeyboardExtension.iOS 项目中对 Walterlv.KeyboardExtension.Keyboard 项目的引用节点的话,你会发现 Xamarin 已经自动为这个项目标记上了 <IsAppExtension />
。只有加上了 AppExtension 标记,Xamarin 才会把这个项目作为 iOS 扩展项目进行打包。
<ProjectReference Include="..\..\Walterlv.KeyboardExtension.Keyboard\Walterlv.KeyboardExtension.Keyboard.csproj">
<Project>{d6f006e7-3c98-4b97-b2d5-4d2e3bc2f945}</Project>
<Name>Walterlv.KeyboardExtension.Keyboard</Name>
<IsAppExtension>true</IsAppExtension>
<IsWatchApp>false</IsWatchApp>
</ProjectReference>
在以上三个步骤完成之后,理论上你是可以正常编译此项目的。
iOS 应用的包信息存储在 plist 中。所以在这一节,你需要正确配置两个项目的 plist。
没错!是两个项目。还记得前面背景知识里面我们说到容器项目和扩展项目就是两个不同的应用吗?
配置 plist 的方法,就是在 Visual Studio 里面双击这个文件。
按照下图这样配置:
说明:
至此,你的项目可以直接编译了。如果你有真机部署环境,都可以直接部署到真机上看效果了。
本文不会花篇幅来讲如何真机部署调试,不然这篇文章就没有重点。
但是你可以阅读:使用 Xamarin 在 iOS 真机上部署应用进行调试
当然这是 Mac 版本的(毕竟我在 Windows 上实际也没有成功真机调试过,我是 git 同步到 Mac 上用 Visual Studio for Mac 来真机调试的)。
只是你需要注意做这些内容:
下面是我部署到真机上之后,在亮暗两种不同的界面下的键盘截图(就是上面的项目,没有改任何代码):
我们把 Walterlv.CloudKeyboard.iOS.Extension 也就是那个键盘扩展项目删除得只剩下 KeyboardViewController.cs 了,我们也只需要在这个类中写代码而已。
要控制文字输入,就是使用 TextDocumentProxy
实例。我们的 KeyboardViewController
继承自 UIInputViewController
,于是我们能够在类中直接使用 TextDocumentProxy
实例。
在光标处插入文字:
TextDocumentProxy.InsertText("walterlv");
如果要插入换行或者确认输入,则使用:
TextDocumentProxy.InsertText("\n");
在光标处删除前一个字:
TextDocumentProxy.DeleteBackward();
如果想要清空文本,则可以循环删除:
while (TextDocumentProxy.HasText)
{
TextDocumentProxy.DeleteBackward();
}
你没有办法删除后一个字,也不能获取到用户输入的任何内容。
关于换行,特别注意:如果文本框被设置为发送或者其他非换行的功能,那么使用 InsertText
单独插入换行时才能正常执行这些功能。如果调用此代码之前还有其他的插入文字,那么最终就只会是换行,而不会执行其他的功能。实际上我在这一点上踩了坑,导致在 QQ 或者其他工具中只能实现换行,而无法发送消息。
iOS 的键盘有不同种类的确认,需要键盘针对 TextDocumentProxy.
我还没有找到办法直接完成文本的输入,例如执行确认按钮的逻辑。而确认按钮有这么些不同的情况:
// 我当然是写 C# 语言版本的枚举,而不是 Object-C 版本的啦。
public enum UIReturnKeyType : long
{
Default,
Go,
Google,
Join,
Next,
Route,
Search,
Send,
Yahoo,
Done,
EmergencyCall,
Continue,
}
纯本地的键盘很难在打字速度上获得优势,各种主流的输入法也通常借助网络来提高自身的输入准确度。
用户需要在键盘设置里面开启键盘的“允许完全访问”才能让对应的输入法获得网络访问的权限。如果用户没有给权限,那么网络访问的时候键盘扩展就会出现异常,然后闪退。
然而如果你去我们刚刚开发的输入法中看,你会发现我们的输入法没有提供这样的选项可以设置。那么如何能够添加这个设置以便进行网络访问呢?
方法是修改键盘扩展项目的 Info.plist 文件。这个时候的修改,我们就不能使用 Visual Studio 中自带的 plist 编辑器了,我们需要使用文本编辑器来编辑 plist 文件。
在你的 Info.plist 文件中找到 RequestsOpenAccess
属性,然后将它分值从 false
改为 true
:
<key>RequestsOpenAccess</key>
-- <false/>
++ <true/>
这个属性设为 true
之后,再次部署,你将可以在你的键盘设置里面看到“允许完全访问”的设置项。开启之后,你就能在你的键盘里面访问网络了。
一般来说你不用阅读这一小节的内容。因为现在基本上各种服务都已经是 https 了,http 基本已经绝迹。但是如果你需要临时部署一个服务,没来得及申请 https 证书的话,那么就需要使用本小结的内容让你的键盘支持 http 的访问。
继续打开你的键盘扩展项目的 Info.plist 文件,在根字典的最后添加一个完整的字典属性 NSAppTransportSecurity
:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>walterlv.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
特别注意,里面的 walterlv.com
需要换成你自己的域名。是域名,不用包含端口号。
这样,你就能在键盘中访问 http://walterlv.com 了。
如果你还遇到了一些其他诡异的问题:
参考资料
[iOS 8 Custom Keyboard Tutorial: How to Create A Third-Party Keyboard Extension | iPhone and iOS App UI Design Templates](http://www.appdesignvault.com/ios-8-custom-keyboard-extension/#a_aid=mdev) |