在WP7下自定义RelativeSource 的Binding

最近老没有时间上来写博文。今天有空来写写上次还没有说完的话题。上一篇提到说说我在WP7应用开发中遇到的 子控件 DataTemplete 中的按钮的命令绑定,刚开始接触似乎是个头疼的问题。那怎么解决呢?

我们仔细想想 silverlight 就是一个庞大的Composite组合模式的实现。包括了我们所说的可视树。不管怎么加Templete,怎么绑定,最终会出现在可视树上。 好就这么探索去吧。。  

你会发现Silverlight, WP7中有类似功能的一些绑定中的 RelativeSource。但好像只是预留的接口,有些没有实现。

首先,观看 silverlight4中有未实现完整的相对绑定。

那我们自己来做吧!我们一些基础建设也抄他们的,呵呵

首先定义一些基础枚举:

ContractedBlock.gif ExpandedBlockStart.gif View Code
public enum RelativeSourceMode
{
ParentDataContext
= 0,

FindAncestor
= 1

}
}

肯定需要一些相当绑定数据信息,同样参考一下Silverlight4

绑定可能会在同一个控件绑定几个事件,属性所以需要一个集合来管理吧,好做一个简单的。

怎么能给控件一个绑定自定义绑定属性呢? Attache 附加属性 是天生的绝配啊, 给注册一个附加Binding属性 让他可以绑定一个类

,所以做一个抽象的先让数据信息和集合都需继承,这样才能都实用啊。而集合又不需要其他的属性继承下得来,没有意义。 干脆搞个空类:

ContractedBlock.gif ExpandedBlockStart.gif View Code
public abstract class RelativeSourceBase
{
protected RelativeSourceBase()
{

}
}

参考一下Silverlight4实现相当绑定数据收集工作。

ContractedBlock.gif ExpandedBlockStart.gif View Code
    public class RelativeSourceBinding : RelativeSourceBase
{

/// <summary>
/// Gets or sets the path to the binding source property.
/// </summary>
public string Path { get; set; }

/// <summary>
/// Gets or sets the name of the target dependency property.
/// </summary>
public string TargetProperty { get; set; }

/// <summary>
/// Gets or sets the XAML namespace. This namespace is used to get the class of an attached dependency property.
/// </summary>
public string TargetNamespace { get; set; }

/// <summary>
/// Gets or sets the type of ancestor to look for.
/// Define the full name (namespace and class name). Xaml Namespace do not work here.
/// Example: MyNamespace.MyUserControl or System.Windows.ListBox.
/// For types in System.Windows.dll you can just use the class name instead of the full name.
/// </summary>
public string AncestorType { get; set; }

// not implemented yet
//public int AncestorLevel { get; set; }

/// <summary>
/// Gets or sets a value that describes the location of the binding source relative
/// to the position of the binding target.
/// </summary>
public RelativeSourceMode RelativeMode { get; set; }

/// <summary>
/// Gets or sets a value that indicates the direction of the data flow in the binding.
/// </summary>
public BindingMode BindingMode { get; set; }

/// <summary>
/// Gets or sets the converter object that is called by the binding engine to
/// modify the data as it is passed between the source and target, or vice versa.
/// </summary>
public IValueConverter Converter { get; set; }

/// <summary>
/// Gets or sets the culture to be used by the System.Windows.Data.Binding.Converter.
/// </summary>
public CultureInfo ConverterCulture { get; set; }

/// <summary>
/// Gets or sets a parameter that can be used in the System.Windows.Data.Binding.Converter logic.
/// </summary>
public object ConverterParameter { get; set; }

/// <summary>
/// Gets or sets a value that indicates whether the System.Windows.FrameworkElement.BindingValidationError event is raised on validation errors.
/// </summary>
public bool NotifyOnValidationError { get; set; }

/// <summary>
/// Gets or sets a value that indicates whether the binding engine will report
/// validation errors from an System.ComponentModel.IDataErrorInfo implementation
/// on the bound data entity.
/// </summary>
public bool ValidatesOnDataErrors { get; set; }

/// <summary>
/// Gets or sets a value that indicates whether the binding engine will report
/// exception validation errors.
/// </summary>
public bool ValidatesOnExceptions { get; set; }

/// <summary>
/// Gets or sets a value that indicates whether the binding engine will report
/// validation errors from an System.ComponentModel.INotifyDataErrorInfo implementation
/// on the bound data entity.
/// </summary>
public bool ValidatesOnNotifyDataErrors { get; set; }

internal DependencyProperty TargetDependencyProperty { get; set; }
}

在搞个List来管理多个绑定,让其也继承基类

ContractedBlock.gif ExpandedBlockStart.gif View Code
public class BindingList : RelativeSourceBase, IList<RelativeSourceBinding>, ICollection<RelativeSourceBinding>, IEnumerable<RelativeSourceBinding>, IList, ICollection, IEnumerable
{

private List<RelativeSourceBinding> _internalList;

public BindingList()
{
_internalList
= new List<RelativeSourceBinding>();
}

#region IList<RelativeSourceBinding> Members

public int IndexOf(RelativeSourceBinding item)
{
return _internalList.IndexOf(item);
}

public void Insert(int index, RelativeSourceBinding item)
{
_internalList.Insert(index, item);
}

public void RemoveAt(int index)
{
_internalList.RemoveAt(index);
}

public RelativeSourceBinding this[int index]
{
get
{
return _internalList[index];
}
set
{
_internalList[index]
= value; ;
}
}

#endregion

#region ICollection<RelativeSourceBinding> Members

public void Add(RelativeSourceBinding item)
{
_internalList.IndexOf(item);
}

public void Clear()
{
_internalList.Clear();
}

public bool Contains(RelativeSourceBinding item)
{
return _internalList.Contains(item);
}

public void CopyTo(RelativeSourceBinding[] array, int arrayIndex)
{
_internalList.CopyTo(array, arrayIndex);
}

public int Count
{
get { return _internalList.Count; }
}

public bool IsReadOnly
{
get { return false; }
}

public bool Remove(RelativeSourceBinding item)
{
return _internalList.Remove(item);
}

#endregion

#region IEnumerable<RelativeSourceBinding> Members

public IEnumerator<RelativeSourceBinding> GetEnumerator()
{
return _internalList.GetEnumerator();
}

#endregion

#region IEnumerable Members

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return _internalList.GetEnumerator();
}

#endregion

#region IList Members

int IList.Add(object value)
{
_internalList.Add((RelativeSourceBinding)value);
return (this.Count - 1);
}

void IList.Clear()
{
_internalList.Clear();
}

bool IList.Contains(object value)
{
return _internalList.Contains((RelativeSourceBinding)value);
}

int IList.IndexOf(object value)
{
return _internalList.IndexOf((RelativeSourceBinding)value);
}

void IList.Insert(int index, object value)
{
_internalList.Insert(index, (RelativeSourceBinding)value);
}

bool IList.IsFixedSize
{
get { return false; }
}

bool IList.IsReadOnly
{
get { return false; }
}

void IList.Remove(object value)
{
_internalList.Remove((RelativeSourceBinding)value);
}

void IList.RemoveAt(int index)
{
_internalList.RemoveAt(index);
}

object IList.this[int index]
{
get
{
return _internalList[index];
}
set
{
_internalList[index]
= (RelativeSourceBinding)value;
}
}

#endregion

#region ICollection Members

void ICollection.CopyTo(Array array, int index)
{
throw new NotImplementedException();
}

int ICollection.Count
{
get { return _internalList.Count; }
}

bool ICollection.IsSynchronized
{
get { return false; }
}

object ICollection.SyncRoot
{
get { return _internalList; }
}

#endregion
}

数据搞定了,现在主攻逻辑实现了。。

1.实现附加属性。

2.分析附加属性绑定配置数据信息。

3.查找可视树找到对应UI元素

4.创建绑定。这是时候遇到了问题,需要多重"."属性分析。 还有绑定目标中是否通过名称怎么分析Type? 还好查资料有很多类型的分析代码。

ContractedBlock.gif ExpandedBlockStart.gif View Code
[ContentProperty("Binding")]
public static class BindingAdapter
{

#region Binding (Attached DependencyProperty)

public static RelativeSourceBase GetBinding(DependencyObject obj)
{
return (RelativeSourceBase)obj.GetValue(BindingProperty);
}

public static void SetBinding(DependencyObject obj, RelativeSourceBase value)
{
obj.SetValue(BindingProperty, value);
}

public static readonly DependencyProperty BindingProperty = DependencyProperty.RegisterAttached("Binding", typeof(RelativeSourceBase), typeof(BindingAdapter), new PropertyMetadata(null, OnBinding));

private static void OnBinding(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
FrameworkElement targetElement
= depObj as FrameworkElement;

if (targetElement != null)
{
// attach loading event
targetElement.Loaded += new RoutedEventHandler(targetElement_Loaded);
targetElement.Unloaded
+= new RoutedEventHandler(targetElement_Unloaded);
}
}

#endregion


#region Private methods

private static void targetElement_Loaded(object sender, RoutedEventArgs e)
{
try
{
FrameworkElement targetElement
= sender as FrameworkElement;

// release handler to prevent memory leaks
targetElement.Loaded -= new RoutedEventHandler(targetElement_Loaded);

RelativeSourceBase bindings
= GetBinding(targetElement);


if (bindings is RelativeSourceBinding)
{
// get the binding configuration
RelativeSourceBinding bindingConfiguration = bindings as RelativeSourceBinding;
ProcessBinding(targetElement, bindingConfiguration);
}
else if (bindings is BindingList)
{
// get the binding configuration
BindingList list = bindings as BindingList;

foreach (RelativeSourceBinding bindingConfiguration in list)
{
ProcessBinding(targetElement, bindingConfiguration);
}
}

}
catch (Exception ex)
{
// ignore this exception, because the SL binding engine does not throw exceptions when a binding is wrong.
}
}

static void targetElement_Unloaded(object sender, RoutedEventArgs e)
{
try
{
FrameworkElement targetElement
= sender as FrameworkElement;
targetElement.Unloaded
-= new RoutedEventHandler(targetElement_Unloaded);

RelativeSourceBase bindings
= GetBinding(targetElement);

if (bindings is RelativeSourceBinding)
{
// get the binding configuration
RelativeSourceBinding bindingConfiguration = bindings as RelativeSourceBinding;
if (bindingConfiguration.TargetDependencyProperty != null)
targetElement.ClearValue(bindingConfiguration.TargetDependencyProperty);
}
else if (bindings is BindingList)
{
// get the binding configuration
BindingList list = bindings as BindingList;

foreach (RelativeSourceBinding bindingConfiguration in list)
{
if (bindingConfiguration.TargetDependencyProperty != null)
targetElement.ClearValue(bindingConfiguration.TargetDependencyProperty);
}
}
}
catch
{
}
}

private static void ProcessBinding(FrameworkElement targetElement, RelativeSourceBinding bindingConfiguration)
{

if (bindingConfiguration.RelativeMode == RelativeSourceMode.FindAncestor &&
!string.IsNullOrEmpty(bindingConfiguration.AncestorType))
{
// navigate up the tree to find the type
DependencyObject currentObject = VisualTreeHelper.GetParent(targetElement);

DependencyObject candidate
= null;
DependencyObject ancestor
= null;

while (true)
{
if (currentObject == null)
{
break;
}

Type currentType
= currentObject.GetType();

while (currentType != null && currentType.IsSubclassOf(typeof(DependencyObject)))
{
if (currentType.FullName == bindingConfiguration.AncestorType)
{
ancestor
= currentObject;
break;
}

// for types in assemblies System.Windows, System.Windows.Controls, System.Windows.Controls.Data, etc,
// its possible to define just the class name instead of the full class name including the namespace.
if (candidate == null && currentType.Name == bindingConfiguration.AncestorType && currentType.Assembly.FullName.StartsWith("System.Windows"))
{
// the name of the element is matching, but it is not the fullname.
// remeber the element in case if no element is matching to the ancestor type name
candidate = currentObject;
}

// next type up the hierarchy
currentType = currentType.BaseType;
}

// next parent
currentObject = VisualTreeHelper.GetParent(currentObject);
}

// concrete
if (ancestor == null)
{
ancestor
= candidate;
}

if (ancestor != null && ancestor is FrameworkElement)
{
// bind them
CreateBinding(targetElement, ancestor, bindingConfiguration);
}
}
else if (bindingConfiguration.RelativeMode == RelativeSourceMode.ParentDataContext)
{
object currentDataContext = targetElement.DataContext;

// navigate up the tree to find the parent datacontext
DependencyObject currentObject = VisualTreeHelper.GetParent(targetElement);

while (true)
{
if (currentObject == null)
break;

FrameworkElement fe
= currentObject as FrameworkElement;

if (fe != null)
{
if (fe.DataContext != null && fe.DataContext != currentDataContext)
{
// bind them
CreateBinding(targetElement, fe.DataContext, bindingConfiguration);
break;
}
}

// next parent
currentObject = VisualTreeHelper.GetParent(currentObject);
}

}
}


private static List<string> GetClassNames(Type type)
{
List
<string> result = new List<string>();

// check
if (type == null && type.IsSubclassOf(typeof(DependencyObject)))
return result;

// process
do
{
result.Add(type.FullName);
type
= type.BaseType;
}
while (type != null && type.IsSubclassOf(typeof(DependencyObject)));

// return
return result;
}

private static void CreateBinding(FrameworkElement targetElement, object sourceElement, RelativeSourceBinding bindingConfiguration)
{
// input check
if (targetElement == null)
return;
if (sourceElement == null)
return;
if (bindingConfiguration == null)
return;

// check binding configuration
// ...target property must be set
if (bindingConfiguration.TargetProperty.IsNullOrWhiteSpace())
return;
// ...path property must be set
if (bindingConfiguration.Path.IsNullOrWhiteSpace())
return;



// support of attached property binding syntax: TargetProperty='(Grid.Row)'
string targetPropertyName = (bindingConfiguration.TargetProperty + "").Trim().TrimStart('(').TrimEnd(')') + "Property";

// find the target dependency property
DependencyProperty targetDependencyProperty = null;
if (targetPropertyName.Contains("."))
{
// it is an attached dependency property
string[] parts = targetPropertyName.Split('.');

if (parts.Length == 2 && !parts[0].IsNullOrWhiteSpace() && !parts[1].IsNullOrWhiteSpace())
{
Type attachedType
= TypeLoader.GetType(parts[0], bindingConfiguration.TargetNamespace);

if (attachedType != null)
{
FieldInfo[] targetFields
= attachedType.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
FieldInfo targetDependencyPropertyField
= targetFields.FirstOrDefault(i => i.Name == parts[1]);
if (targetDependencyPropertyField != null)
targetDependencyProperty
= targetDependencyPropertyField.GetValue(null) as DependencyProperty;
}
}
}
else
{
// it is a standard dependency property
FieldInfo[] targetFields = targetElement.GetType().GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
FieldInfo targetDependencyPropertyField
= targetFields.FirstOrDefault(i => i.Name == targetPropertyName);

if (targetDependencyPropertyField != null)
targetDependencyProperty
= targetDependencyPropertyField.GetValue(null) as DependencyProperty;
}


// set binding
if (targetDependencyProperty != null)
{

if (bindingConfiguration.TargetDependencyProperty != null
&& bindingConfiguration.TargetDependencyProperty != targetDependencyProperty)
targetElement.ClearValue(bindingConfiguration.TargetDependencyProperty);

bindingConfiguration.TargetDependencyProperty
= targetDependencyProperty;


Binding binding
= new Binding();
binding.Source
= sourceElement;
binding.Path
= new PropertyPath(bindingConfiguration.Path);
binding.Mode
= bindingConfiguration.BindingMode;
binding.Converter
= bindingConfiguration.Converter;
binding.ConverterParameter
= bindingConfiguration.ConverterParameter;
binding.ConverterCulture
= bindingConfiguration.ConverterCulture;
binding.NotifyOnValidationError
= bindingConfiguration.NotifyOnValidationError;
#if !WINDOWS_PHONE
binding.ValidatesOnDataErrors
= bindingConfiguration.ValidatesOnDataErrors;
#endif
binding.ValidatesOnExceptions
= bindingConfiguration.ValidatesOnExceptions;
#if !WINDOWS_PHONE
binding.ValidatesOnNotifyDataErrors
= bindingConfiguration.ValidatesOnNotifyDataErrors;
#endif
// set the binding on our target element
targetElement.SetBinding(targetDependencyProperty, binding);
}
}



#endregion
}

也把这个XMAL类型分析的小工具代码转给大家吧

ContractedBlock.gif ExpandedBlockStart.gif View Code
/// <summary>
/// Provides functionality to load any type with its class name, namespace and assembly-name within the Silverlight environment.
/// </summary>
/// <remarks>
/// The Type.GetType method is different in Silverlight than in the standard .NET runtime. In Silverlight we have to provide the
/// fully qualified assembly name to get a type in a custom assembly. Only build in controls or types in the same assembly are
/// excluded from this rule. Full qualified assembly name means a syntax like the following:
/// MyComponent.MyType, MyAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4bec85d7bec6698f.
/// This class uses the XamlReader capability to resolve type during parsing a xaml-string. While this is a little time consuming
/// the TypeLoader maintains a cache to get types faster.
/// </remarks>
public static class TypeLoader
{
// cache for resolved type
private static Dictionary<string, Type> _cache = new Dictionary<string, Type>();

/// <summary>
/// Gets the System.Type with the specified name, name space and assembly name.
/// </summary>
/// <param name="className">The class name without namespace.</param>
/// <param name="nameSpace">The name space</param>
/// <param name="assemblyName">The name of the assembly containing the type.</param>
/// <returns>The type matching the provided parameters or null if not found.</returns>
//[DebuggerStepThrough()]
public static Type GetType(string className, string nameSpace, string assemblyName)
{
// check
if (nameSpace.IsNullOrWhiteSpace())
return null;

string xamlNamespace = string.Format("clr-namespace:{0}", nameSpace);
// assembly name is optional
if (!assemblyName.IsNullOrWhiteSpace())
xamlNamespace
+= string.Format(";assembly={0}", assemblyName);

return GetType(className, xamlNamespace);
}

/// <summary>
/// Gets the System.Type with the specified name.
/// This method overload can be used for:
/// 1. core controls such as Button, Grid, ListBox, etc. without specifying the namespace or assembly name.
/// 2. with the qualified assembly name of the type without version and public key token like this: "MyNamespace.MyType, MyAssembly".
/// </summary>
/// <param name="className">Pure class name of Core Controls such as Button, Grid, ListBox, etc.</param>
/// <returns>The type matching the provided parameters or null if not found.</returns>
//[DebuggerStepThrough()]
public static Type GetType(string className)
{
if (className != null && className.Contains(","))
{
string[] qualifiedNameParts = className.Split(',');

if (qualifiedNameParts.Length == 2)
{
string[] fullClassNameParts = qualifiedNameParts[0].Split('.');

if (fullClassNameParts.Length > 0)
{
// classname
string newClassName = fullClassNameParts.Last().Trim();

// namespace
string nameSpace = "";
for (int i = 0; i < fullClassNameParts.Length - 1; i++)
{
nameSpace
+= fullClassNameParts[i] + ".";
}
nameSpace
= nameSpace.TrimEnd('.');

string assemblyName = qualifiedNameParts[1].Trim();

return GetType(newClassName, nameSpace, assemblyName);
}
}

}

return GetType(className, "");
}

/// <summary>
/// Gets the System.Type with the specified name. The xaml namespace specifies the namespace and assembly name in the same syntax as in xaml.
/// </summary>
/// <param name="className">The class name without namespace.</param>
/// <param name="xamlNamespace">
/// The xaml namespace. This is the same syntax as used in XAML syntax.
/// Example: "clr-namespace:MyComponent.SubNamespace;assembly=MyAssemblyName
/// </param>
/// <returns>The type matching the provided parameters or null if not found.</returns>
//[DebuggerStepThrough()]
public static Type GetType(string className, string xamlNamespace)
{
// check input
if (className.IsNullOrWhiteSpace())
return null;

if (className.Contains("."))
throw new ArgumentException("className must not include the namespace. Please provide namespace with separate parameter.");

// check if type is already in cache
string key = xamlNamespace + "&" + className;

if (_cache.ContainsKey(key))
return _cache[key];


lock (_cache)
{
try
{
// check again because another thread might be faster and has already created the cache-entry
if (_cache.ContainsKey(key))
return _cache[key];

// create xaml with a simply Style element and set the TargetType property with the provided type name
string xaml = "<Style xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' ";

// set the xaml namesapce if provided
if (!xamlNamespace.IsNullOrWhiteSpace())
{
xaml
+= string.Format("xmlns:tmp='{0}' TargetType='tmp:{1}' />", xamlNamespace, className);
}
else
{
// Core controls such as Button, Grid, ListBox, etc do not need a namespace
xaml += string.Format("TargetType='{0}' />", className);
}

// let the XamlParser load the type via the TargetType property
Style style = XamlReader.Load(xaml) as Style;

if (style != null)
{
Type targetType
= style.TargetType;
AddToCache(key, targetType);
return targetType;
}
}
catch (Exception ex)
{
// Try to load type in executing assembly
if (!xamlNamespace.IsNullOrWhiteSpace())
{
// note: Type.GetType uses needs assembly-qualified name of the type to get. If the type is
// in the currently executing assembly or in Mscorlib.dll, it is sufficient to supply
// the type name qualified by its namespace.
Type type = Type.GetType(string.Format("{0}.{1}", xamlNamespace.Replace("clr-namespace:", "").TrimEnd(';'), className));

if (type != null)
{
// add to cache
AddToCache(key, type);
return type;
}
}

//****** DONT SET VALUE TO NULL, BECAUSE OF CASES WHEN AN ASSEMBLY IS *****
//****** LOADED DYNAMICALLY INTO THE APPLICATION DOMAIN *****
// don't let the exception repeat. Set null as cache value
AddToCache(key, null);
//**************************************************************************/
}
}

return null;
}

private static void AddToCache(string key, Type type)
{
_cache.Add(key, type);
CompositionTarget.Rendering
-= new EventHandler(CompositionTarget_Rendering);
CompositionTarget.Rendering
+= new EventHandler(CompositionTarget_Rendering);
}

static void CompositionTarget_Rendering(object sender, EventArgs e)
{
CompositionTarget.Rendering
-= new EventHandler(CompositionTarget_Rendering);
_cache.Clear();
}

}

至此快成功了试一下:

<my:CommandButton CommandParameter="{Binding}" >
       <localBinding:BindingAdapter.Binding>
              <localBinding:RelativeSourceBinding Path="ClickCommmand" TargetProperty="Command"
                                 RelativeMode="ParentDataContext" />

               </localBinding:BindingAdapter.Binding>

......................

<my:CommandButton CommandParameter="{Binding}" >
          <localBinding:BindingAdapter.Binding>
                <localBinding:RelativeSourceBinding Path="DataContext.ClickCommmand" TargetProperty="Command"
                              RelativeMode="FindAncestor" AncestorType="Grid" />

          </localBinding:BindingAdapter.Binding>

.............

ContractedBlock.gif ExpandedBlockStart.gif View Code
<ComboBox Grid.Column="1">
<local:BindingAdapter.Binding>
<local:BindingList>
<local:RelativeSourceBinding Path="DataContext.Picklist" TargetProperty="ItemsSource" RelativeMode="FindAncestor" AncestorType="UserControl" />
<local:RelativeSourceBinding Path="DataContext.Tooltip"
TargetProperty
="(ToolTipService.ToolTip)" RelativeMode="FindAncestor"
AncestorType
="UserControl" />
<local:RelativeSourceBinding Path="DataContext.Tooltip" TargetProperty="(DemoAttachedElement.Value)"
TargetNamespace
="clr-namespace:********;assembly=*******" RelativeMode="FindAncestor" AncestorType="UserControl" />
</local:BindingList>
</local:BindingAdapter.Binding>
</ComboBox>

  

转载于:https://www.cnblogs.com/zhouhoujun/archive/2011/07/18/2109067.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值